@gotcos/glasses-server 6.81.0 → 6.83.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 +93 -0
- package/docs/work-orchestrator.md +184 -0
- package/package.json +6 -1
- package/server/index.ts +19 -0
- package/server/lib/claude-bridge.ts +10 -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-coordinator.ts +5 -1
- 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/lib/work-chat.ts +76 -9
- package/server/lib/work-lens-rounds.ts +445 -0
- package/server/lib/work-orchestrator/discovery.ts +99 -0
- package/server/lib/work-orchestrator/input.ts +109 -0
- package/server/lib/work-orchestrator/pickup.ts +85 -0
- package/server/lib/work-orchestrator/plan-log.ts +38 -0
- package/server/lib/work-orchestrator/redact.ts +41 -0
- package/server/lib/work-orchestrator/runtime.ts +74 -0
- package/server/lib/work-orchestrator/session-log.ts +114 -0
- package/server/lib/work-orchestrator/snapshot.ts +87 -0
- package/server/lib/work-orchestrator/tag-index.ts +272 -0
- package/server/lib/work-orchestrator/tags.ts +52 -0
- package/server/lib/work-orchestrator/usage-events.ts +30 -0
- package/server/lib/work-orchestrator/usage.ts +341 -0
- package/server/routes/routines.ts +101 -0
- package/server/routes/work-chat.ts +54 -11
- package/server/routes/work-lens-rounds.ts +179 -0
- package/server/routes/work-orchestrator.ts +71 -0
- package/server/scripts/work-lens-rounds-mutation-gate.mjs +151 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,96 @@
|
|
|
1
|
+
## 6.83.0 - Work orchestrator: every session on a card, where each left off, and plan usage, in the planner input
|
|
2
|
+
|
|
3
|
+
Miles, 2026-10-20 21:47: the Work chat should act like an orchestrator, not a form: find a card's sessions itself,
|
|
4
|
+
read a session's log to know where to pick up, and fork to whichever of Claude, Codex or Cursor has usage left.
|
|
5
|
+
Decisions (21:55): Auto-send is a per-card switch in COS Control; plan usage only, never a silent key-lane fallback.
|
|
6
|
+
This is M1 plus the planner-input parts of M2 and M3 (`docs/work-orchestrator.md`). The server plans; COS Control
|
|
7
|
+
still owns every send.
|
|
8
|
+
|
|
9
|
+
- **Client matrix.** COS Control 0.5.333 uses it. Older Controls never send `orchestrator: true` and get 6.81.0's
|
|
10
|
+
prompt, schema, parser and output budget byte for byte (`WORK_CHAT_SYSTEM` SHA-256 pinned). This release also
|
|
11
|
+
carries everything in 6.82.0 (routines): the two lines were built in parallel from 6.81.0 and merged here.
|
|
12
|
+
- **Phase A: Control pushes, the server keeps.** `POST /api/work/orchestrator/input {workID, meetingWorkIDs, snapshot}`
|
|
13
|
+
stores Control's bounded Work snapshot (`data/work-orchestrator/control-snapshot.json`, 0600, atomic; ids, modes,
|
|
14
|
+
statuses, times only) and answers the card's planner rows. Nothing there sends, links or calls a model.
|
|
15
|
+
- **Discovery** (canary C3 rules 1 to 8): links, newSession, fork child and continue receipts, the card's COS-WORK
|
|
16
|
+
handoff prompt in a user turn, a valid status line in the session's own reply, the tag in the live row, the card's
|
|
17
|
+
source meeting (a suggestion). Never: tool output, quoted handoffs, subagents, a sourceSessionID. Precision fixes:
|
|
18
|
+
Claude rows credited by their own sessionId, Codex guardian and other child threads skipped (thread_spawn folds into
|
|
19
|
+
its parent), placeholder tags inert, newest strong evidence decides which card a session works on now. The log
|
|
20
|
+
index is incremental (`data/work-orchestrator/tag-index.json`): 8.7 GB, 1,368 files scanned in 10.4 s on Miles's
|
|
21
|
+
Mac, then 112 ms. On the toadstone card it finds both sessions.
|
|
22
|
+
- **Pick-up notes** (M2 input): deterministic, from the last 6 MB of the log (seek from the end), harness notices
|
|
23
|
+
skipped, the live busy flag on top, secrets and customer identifiers (emails, phones, URL paths, amounts, long ids)
|
|
24
|
+
redacted, at most 1,500 characters. No model call.
|
|
25
|
+
- **Plan usage** (M3 input): Claude `get_usage` over stream-json (no turn), Codex `account/rateLimits/read` (never
|
|
26
|
+
`supportsLunaReserve`, never a reset-credit call), Cursor unknown (no Keychain read, no TUI launch). Read on
|
|
27
|
+
demand only: a plan input re-reads a provider whose reading is older than 2 min (the plan is where a fork or a new
|
|
28
|
+
session is chosen); `GET /api/work/orchestrator/usage` re-reads one older than 10 min (2 with `refresh=1`). No
|
|
29
|
+
timer reads usage by default; the 10-minute poll runs only when the owner sets `COS_WORK_USAGE_POLL=1` (Miles has
|
|
30
|
+
not approved a background poll for every user). `rate_limit_event` frames from plan-lane Claude jobs and
|
|
31
|
+
`provider_limit` failures from plan-lane jobs mark a provider exhausted until the stated reset (or 1 h), over any
|
|
32
|
+
cached percentage; a key-lane turn never counts. Three failed probes pause that provider for an hour. Read on this
|
|
33
|
+
Mac at 23:0x: Claude 43% weekly headroom, Codex 8%.
|
|
34
|
+
- **Planner input and answer.** Rows add discoveredBy, linked, member, lastTouchedCard, lastActivity, live, busy,
|
|
35
|
+
pickup, available + reason (not_in_session_list, app_owned, cursor, plan_limit, on_another_card) and forkable; the
|
|
36
|
+
input adds now, usage, leftOut and newSessionProviders. `automatic` stays the only Continue gate. Answers may Fork a
|
|
37
|
+
forkable row or start a New session on an offered provider. `WORK_CHAT_ORCHESTRATOR_RULES` (appended only for an
|
|
38
|
+
orchestrator input): prefer headroom, never an exhausted provider, avoid stale owners, continue vs fork, and never
|
|
39
|
+
say a card has no sessions when rows or leftOut are present.
|
|
40
|
+
- **Every plan logged**, from any client: `data/work-orchestrator/plans.jsonl` (2 MB, newest half kept) and one
|
|
41
|
+
`[work-orchestrator] plan` line: sessions sent and their availability, left out and why, usage age, assignments
|
|
42
|
+
(mode and target, never text), outcome, time.
|
|
43
|
+
- **Lens plans (Miles 2026-10-21 09:11, "Add both").** A plan the lens made can be saved into the card's Work chat
|
|
44
|
+
history, and sent from the lens. `GET /api/work/chat` adds `lensPlans: 1` and `lensPlansControl {connected,
|
|
45
|
+
lastSeenSec, autoSend}` (connected: COS Control polled the lens inbox in the last 60 s). The lens records with
|
|
46
|
+
`POST /api/work/chat/lens-rounds` (201, a repeated `clientRoundId` 200 with the same body; 400 `invalid_lens_round`
|
|
47
|
+
with `field`, 404 `unknown_work`, 413), reads `GET .../lens-rounds/:roundId` (state and per-assignment results, never
|
|
48
|
+
the prompts), asks `POST .../:roundId/send` (202; 409 `already_sent`, `send_in_progress`, `plan_stale`; 423
|
|
49
|
+
`control_not_connected`; 428 `confirmation_required` without `confirm: "two-step"` unless the card's Auto-send is on)
|
|
50
|
+
and `POST .../:roundId/stop`. COS Control, loopback only, polls `GET /api/work/chat/lens-inbox`, marks
|
|
51
|
+
`POST .../:roundId/history` and reports `POST /api/work/chat/lens-send/:sendRequestId/result`. Nothing here sends:
|
|
52
|
+
COS Control performs every send through its own Send path. A send not picked up in 10 minutes is `expired`.
|
|
53
|
+
Storage `data/work-lens-rounds.json` (0600, atomic, 200 rounds, 14 days). Every route answers 503 `maintenance`
|
|
54
|
+
while admissions are closed. Existing routes are unchanged (pinned). Contract: `CONTRACT_lens_plans_6.83.0_2026-10-21.md`.
|
|
55
|
+
QA on 6.10.651 (additive): `POST /api/work/chat/lens-rounds/:roundId/mac-send` (COS Control reports its own Send of a
|
|
56
|
+
lens round on the Mac, so a later lens send never delivers twice; `already_sent` lists the ids, and held counts as
|
|
57
|
+
sent); round outcomes add `failed` and `stopped` (all failed, all stopped), `refused` only for an actual refusal.
|
|
58
|
+
Checks after QA: `work-lens-rounds.test.ts` 32 tests; gate green baseline (40 tests in 2 files), 64 of 64 killed;
|
|
59
|
+
full suite 8,583 passed, 0 failed, 3 skipped (569 files).
|
|
60
|
+
Checks before QA: `work-lens-rounds.test.ts` 25 tests; `server/scripts/work-lens-rounds-mutation-gate.mjs` (not packaged): green
|
|
61
|
+
baseline (33 tests in 2 files), 56 of 56 mutants killed by a named test. Full suite after: 8,576 passed, 0 failed,
|
|
62
|
+
3 skipped (569 files).
|
|
63
|
+
- **Checks.** Full suite 8,506 passed, 0 failed, 3 skipped (563 files); `tsc --noEmit` clean. The first full run
|
|
64
|
+
failed (9 Claude bridge suites that mock `node:child_process` could not load the orchestrator runtime; the key-lanes
|
|
65
|
+
P6 census had an unclassified site): the bridges now report through a leaf module and the site is classified.
|
|
66
|
+
`server/scripts/work-orchestrator-mutation-gate.mjs` (not packaged): green baseline (62 tests in 8 files), 70 mutants,
|
|
67
|
+
68 killed by a named test, 2 documented survivors (a seek-cut row never parses; the key check refuses `mode` from an
|
|
68
|
+
older client first). The gate found 11 real gaps on its first pass, each closed by a test (and fixed its own `$&`
|
|
69
|
+
replacement bug). Journey: the toadstone card replay (fake logs in the 2026-10-20 shapes) finds both sessions, plans
|
|
70
|
+
with them, and never says "no sessions"; the 6.81.0 path on the same card saw none. Real probes on Miles's Mac read
|
|
71
|
+
Claude and Codex usage with no turn sent. Shadow replay C4 (10 cards, 10 Opus calls, nothing sent): see the hand-off.
|
|
72
|
+
- **Release checks (merged 6.82.0 + 6.83.0, after the on-demand usage change).** `tsc --noEmit` clean. Full suite
|
|
73
|
+
8,550 passed, 1 failed, 3 skipped (568 files): the failure is ST-3 in `query-job-steer-coordinator.test.ts`, a
|
|
74
|
+
fixed 50 ms sleep before reading the steer snapshot; that file and `claude-steer.ts` are unchanged from 6.82.0, the
|
|
75
|
+
carried steer was in the prompt, and the file passed 18 of 18 five runs out of five in isolation. Routines gate:
|
|
76
|
+
green baseline (55 tests in 6 files), 42 of 42 killed. Orchestrator gate: green baseline (66 tests in 8 files), 79
|
|
77
|
+
mutants, 77 killed by a named test, the same 2 documented survivors; the 10 new mutants, which replace the old `usage-poll-switch` (the opt-in switch and its
|
|
78
|
+
gate, the on-demand read, its 10-minute cache and 2-minute refresh, the plan's 2-minute re-read, the input and route
|
|
79
|
+
wiring) are all killed. `npm pack --dry-run`: 536 files, every new routines and orchestrator source file present,
|
|
80
|
+
no tests, fixtures, gates or canary.
|
|
81
|
+
|
|
82
|
+
## 6.82.0 - Routines: any COS skill on a schedule, each with its own cap, off switch and breaker
|
|
83
|
+
|
|
84
|
+
Miles, 2026-10-20 (plan Decision Q4): "Any COS skill on a schedule. Pick a skill, a time and days; each routine has its own cap and off switch." Until now the server had one routine, the morning brief.
|
|
85
|
+
|
|
86
|
+
- **Client matrix.** Server only in this release. Companion 6.10.650 lists, adds, edits, pauses and runs routines from the phone; older companions and COS Control keep using `/api/morning-brief`, which is unchanged. Nothing runs that a user did not create: a fresh server has the morning brief and no skill routines.
|
|
87
|
+
- **The morning brief is routine #1, unchanged.** Its config file, scheduler, prompt, scheduled submission, `/api/morning-brief` envelope, a rejected PUT and Run now while it runs are pinned by `morning-brief-golden.test.ts`, recorded on the unmodified 6.81.0 base (6b2e70b) before any routine code existed and passing byte for byte after. The only change it sees: its run is counted in the token audit as `routine:morning-brief` instead of `full_query`.
|
|
88
|
+
- **`/api/routines`.** `GET` lists the brief first, then skill routines, each with schedule, next run, gate (why it will not run), breaker state, today's runs against its cap, and its last run. `GET /routines/skills` lists this workspace's skills (`.agents/skills`, `.claude/skills`; labels only, never paths). `POST` creates; `PATCH` (or `PUT`) edits, and `resetBreaker: true` closes an open breaker; `POST /:id/pause` and `/:id/resume`; `DELETE` (the brief can only be turned off: 409 `cannot_delete_brief`); `POST /:id/run` is Run now (202; 409 running, off or skill missing; 423 breaker open, with `openUntil`; 429 capped); `GET /:id/runs` and `/routines/runs` are history. A brief edit through `/api/routines/morning-brief` takes only the brief's own fields and refuses any other (400 `not_a_brief_field`).
|
|
89
|
+
- **A skill routine.** A workspace skill, an optional extra instruction, a time and days in a zone (default the Mac's, weekdays at 08:00), runs a day (default 2, at most 10), a model (default `sonnet`, never left to the user default, never a key lane), and its breaker (default 3 failures, 6 hours). Stored in `data/routines/routines.json`, runs in `data/routines/runs.json` (0600, atomic; a damaged file keeps its good routines and drops the rest). It runs as an ordinary durable job with `origin: { kind: 'routine', id }`, so it survives the phone sleeping and lands in the inbox with a message number. Its prompt says nobody is watching: read and summarise freely, write only the skill's own files, never send, publish, deploy, push, delete or spend; if a source is missing, say so in one line.
|
|
90
|
+
- **How it stops.** Its own switch, and `COS_ROUTINES=0` turns every skill routine off (the brief keeps its own switch). Its daily cap counts scheduled and Run now runs together; a failed submit never ran and is not counted. Every skill routine together is capped at 40 runs a day. The breaker opens after N consecutive failed runs (a failed or interrupted job, or a failed submit) and nothing fires until the cooldown ends; then one trial run is allowed, a success closes it and a failure opens it again. A canceled run counts neither way, and each finished run is counted once however many ticks read it. One scheduled slot per selected day, three submit attempts two minutes apart, crash-resumed by a fixed client id. Inert while Background jobs are off or maintenance is closed, as the brief.
|
|
91
|
+
- **Token audit by caller.** A routine's run is logged as `routine:<id>` (Claude, Codex, Cursor and direct bridges; the runtime passes `auditCaller` only for routine origins, so every other caller is byte for byte what it was).
|
|
92
|
+
- **Checks.** Canary C7 (`server/scripts/routines-canary.ts`, not packaged): on a scratch data dir, a scratch HOME and workspace, an ephemeral port, and a stub `claude` on PATH, two routines fired at their slot on the production 30 s tick; the one whose skill fails exited 1, the breaker counted it, a second failure opened it, Run now answered 423 and the next tick skipped it, the other routine stayed ready, and the token audit row read `routine:<id>` on `sonnet`. Mutation gate `server/scripts/routines-mutation-gate.mjs` (not packaged): green baseline (55 tests in 6 files), 42 of 42 mutants killed, each by a named test. Full suite 8,503 passed, 0 failed, 3 skipped (562 files); `tsc --noEmit` clean. The key-lanes census classifies the three new model sites as Reject.
|
|
93
|
+
|
|
1
94
|
## 6.81.0 - The Mac saves a stopped meeting on its own once it holds every chunk
|
|
2
95
|
|
|
3
96
|
Miles, 2026-10-20: "if the application isn't in the foreground, it can fail to save the meetings". Both G2 meetings that day saved only once the phone came back on screen: the phone's Stop needs IndexedDB, and iOS suspends IndexedDB while the companion is hidden (11:19 qbbgyq: Stop 12:43, `stop_blocked`, "Load failed", saved 12:47 by a new WebView; 14:07 mip9qb: Stop 15:13:50, saved 15:14:37 only after the phone was visible). The full map is `docs/meeting-autonomous-save.md`.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Work orchestrator (server 6.83.0, COS Control 0.5.333)
|
|
2
|
+
|
|
3
|
+
Plan: `operations/personal/wk43_2026/PLAN_work_orchestrator_glasses_work_routines_2026-10-20.md` (MU repo), approved by
|
|
4
|
+
Miles 2026-10-20 21:55. Canaries: `CANARY_C1_usage_2026-10-20.md`, `CANARY_C2_C4_orchestrator_2026-10-20.md`.
|
|
5
|
+
This milestone is M1 plus the planner-input parts of M2 (pick-up notes) and M3 (plan usage).
|
|
6
|
+
|
|
7
|
+
## Phase A: how Work state reaches the server (decided 2026-10-20)
|
|
8
|
+
|
|
9
|
+
Work state lives in COS Control today: confirmed links in `work-handoffs/handoffs.session-cards.json`, the receipt
|
|
10
|
+
journal in `handoffs.json` (flock'd, compare-and-swap writes, 10 MB cap), and Work chat rounds in `task-chat/*.json`.
|
|
11
|
+
The planner runs in the server. Two ways to join them were on the table.
|
|
12
|
+
|
|
13
|
+
| | A. Control pushes a snapshot through the helper | B. The server reads Control's files on this Mac |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| Who owns the bytes | Control writes, under its own journal lock; the server keeps a copy | The server reads a journal Control may be writing (it would have to speak Control's lock and CAS protocol) |
|
|
16
|
+
| Contract | A versioned, bounded snapshot with only what discovery needs | Control's private schema, which gains fields almost every release |
|
|
17
|
+
| Paths | None hardcoded in the server | The server hardcodes `~/Library/Application Support/COS Control/...` (the "multi-user path" rule) |
|
|
18
|
+
| Phone, lens, cloud workers | Read the server's copy | Cannot: a cloud worker never sees a Mac file |
|
|
19
|
+
| Freshness | Pushed right before every plan, and after each recorded handoff | Always current on this Mac |
|
|
20
|
+
| Privacy | Ids, modes, statuses, times. Never prompts, results or file names | Everything in the journal, prompts included |
|
|
21
|
+
|
|
22
|
+
**Decision: A.** Control pushes a snapshot on change through the helper (`work-orchestrator-input`), and the server
|
|
23
|
+
stores it atomically in `data/work-orchestrator/control-snapshot.json` (0600). Reasons, in order: Control stays the
|
|
24
|
+
single writer and owner of sends (this milestone keeps its handoff path, receipts, fences and Stop); the snapshot is a
|
|
25
|
+
contract we can version, where Control's journal is not; the server never learns Control's private paths; and the same
|
|
26
|
+
copy serves the phone, the lens and later cloud workers, which B can never do. The one thing B does better, freshness,
|
|
27
|
+
is closed by pushing immediately before each plan (the push and the input build are one helper call) and after each
|
|
28
|
+
handoff Control records. The server reports the snapshot's age in every input it builds, so a stale copy is visible,
|
|
29
|
+
never silent.
|
|
30
|
+
|
|
31
|
+
Snapshot (protocol 1, bounded, refused whole when out of bounds):
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
{ version: 1, controlVersion, sentAt,
|
|
35
|
+
links: [{ sessionID, workID, at }] <= 4000
|
|
36
|
+
receipts: [{ id, workID, mode, provider, sessionID, sourceSessionID,
|
|
37
|
+
forkChildID, status, createdAt }] <= 4000, newest first
|
|
38
|
+
live: { checkedAt, rows: [{ id, title, status, updatedAt, busy,
|
|
39
|
+
waitingOnUser, appOwned }] } <= 400 }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`live` is the session list Control read with `--fresh` immediately before planning. Guardian review rows are left out
|
|
43
|
+
by Control and dropped again by the server.
|
|
44
|
+
|
|
45
|
+
## What the server builds (`POST /api/work/orchestrator/input`)
|
|
46
|
+
|
|
47
|
+
For one card it returns the rows the planner sees, already ranked and bounded to 12, plus what it left out and why,
|
|
48
|
+
plus the usage snapshot. It never sends anything and never changes a link or a receipt.
|
|
49
|
+
|
|
50
|
+
### Discovery (canary C3 rules 1 to 8)
|
|
51
|
+
|
|
52
|
+
| Rank | Evidence | Card session? |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| 1 | Confirmed link | Yes |
|
|
55
|
+
| 2 | `newSession` receipt on the card | Yes |
|
|
56
|
+
| 3 | `fork` receipt with a child (journaled `forkChildID`, or a `sessionID` that differs from `sourceSessionID`) | Yes |
|
|
57
|
+
| 4 | `continueSession` receipt on the card | Yes |
|
|
58
|
+
| 5 | The handoff template for this card's tag in a user turn | Yes (the only way to find fork children) |
|
|
59
|
+
| 6 | A valid `COS-WORK <tag>: done/needs input/blocked` line in the session's own reply | Yes with 1 to 5; alone a suggestion |
|
|
60
|
+
| 7 | The tag in the live row's summary | Yes with 1 to 5; alone a suggestion |
|
|
61
|
+
| 8 | The session worked the card's confirmed source meeting (meeting card `meeting:<recordId>`) | Suggestion, reason shown |
|
|
62
|
+
| Never | Tag in tool output, pasted or quoted handoffs, subagent transcripts, or as `sourceSessionID` | Not a card session |
|
|
63
|
+
|
|
64
|
+
Precision fixes from the canary: Claude rows are credited by their own `sessionId` (a resumed file replays older
|
|
65
|
+
sessions' rows); Codex guardian threads are skipped; Codex `thread_spawn` children fold into their parent; a tag no
|
|
66
|
+
handoff prompt or Control record ever used is ignored (placeholders such as `aaaaaaaaaaaa`); when one session is on
|
|
67
|
+
several cards, its newest strong evidence decides which card it is working on, and the older cards keep it as history
|
|
68
|
+
(`available: false, reason: on_another_card`).
|
|
69
|
+
|
|
70
|
+
The log scan is an incremental index (`data/work-orchestrator/tag-index.json`): append-only logs are read from the last
|
|
71
|
+
offset, so after the first pass only new bytes are scanned. Lines are parsed only when they contain `COS-WORK`.
|
|
72
|
+
|
|
73
|
+
### Rows
|
|
74
|
+
|
|
75
|
+
Each row adds to the existing `{id, title, summary, status, automatic, openWork}`:
|
|
76
|
+
`discoveredBy` (evidence kinds), `linked`, `lastTouchedCard` (newest evidence for this card), `lastActivity`, `live`
|
|
77
|
+
(in the session list Control just read), `pickup` (<= 1,500 characters), `available` and `reason`
|
|
78
|
+
(`not_in_session_list`, `app_owned`, `cursor`, `plan_limit`, `on_another_card`, `busy`), and `forkable`.
|
|
79
|
+
`automatic` stays the only gate for a Continue: Control sets it (Claude or Codex, not app owned, in the live list).
|
|
80
|
+
|
|
81
|
+
### Pick-up notes (M2 input, deterministic)
|
|
82
|
+
|
|
83
|
+
Seek from the end of the log (last 6 MB; Codex logs reach 1.4 GB), parse the last human ask, the last result, whether
|
|
84
|
+
the turn ended, open todos, questions and any COS-WORK status line; harness notices are skipped; the live busy flag
|
|
85
|
+
overrides the state line. Secrets (`redactSecrets`) and customer identifiers (emails, phone numbers, URL paths and
|
|
86
|
+
query strings, money amounts, long numeric ids) are redacted before the note leaves the module. No model call in this
|
|
87
|
+
milestone; the canary's optional Haiku polish stays undecided.
|
|
88
|
+
|
|
89
|
+
### Usage (M3 input, plan usage only)
|
|
90
|
+
|
|
91
|
+
| | Claude | Codex | Cursor |
|
|
92
|
+
| --- | --- | --- | --- |
|
|
93
|
+
| Probe | `claude -p` stream-json control request `get_usage` (`skip_behaviors`), no turn | `codex app-server` `account/rateLimits/read` (`excludeResetCreditDetails`) | none: marked unknown (no Keychain read, no TUI launch; not approved) |
|
|
94
|
+
| Read | on demand: before a plan (POST input) when older than 2 min; GET usage when older than 10 min (2 with `refresh=1`) | same | never |
|
|
95
|
+
| Timer | none by default; every 10 min only with `COS_WORK_USAGE_POLL=1` | same | never |
|
|
96
|
+
| Passive | `rate_limit_event` frames from the server's own plan-lane Claude jobs | none yet | none |
|
|
97
|
+
| Fallback | a `provider_limit` failure marks the provider exhausted until its stated reset, or 1 h | same | same |
|
|
98
|
+
|
|
99
|
+
Never sent: `supportsLunaReserve`. Never called: `ConsumeAccountRateLimitResetCredit`. Codex is gated on
|
|
100
|
+
`ordinaryUsageAllowed === false` first. Probes run in a fresh empty directory, hooks and MCP off, killed by exact PID
|
|
101
|
+
after 15 s. Stops: no background poll unless the owner sets `COS_WORK_USAGE_POLL=1` (Miles has not approved a poll
|
|
102
|
+
for every user; the 6.83.0 release reads usage only when a plan or a usage read asks); three failed probes in a row
|
|
103
|
+
pause that provider's probe for an hour. A key-lane turn's rate-limit frames are never recorded as plan usage.
|
|
104
|
+
|
|
105
|
+
### Planner input and answer
|
|
106
|
+
|
|
107
|
+
The plan request (`POST /api/work/chat`) from an orchestrator client carries `orchestrator: true`, `now` (ISO time),
|
|
108
|
+
`usage` (per provider: state, headroom, reset, age, source), `leftOut` (what discovery left out and why) and
|
|
109
|
+
`newSessionProviders`. The answer may now hold three kinds of assignment:
|
|
110
|
+
|
|
111
|
+
| `mode` | Target | Rule |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| `continue` (default) | `sessionID` | the row is `automatic: true` (unchanged) |
|
|
114
|
+
| `fork` | `sessionID` | the row is `forkable: true` |
|
|
115
|
+
| `newSession` | `provider` | the provider is in `newSessionProviders` |
|
|
116
|
+
|
|
117
|
+
A client that does not send `orchestrator: true` gets today's prompt, schema and parser, byte for byte.
|
|
118
|
+
|
|
119
|
+
### Plan log
|
|
120
|
+
|
|
121
|
+
Every plan, from any client, appends one line to `data/work-orchestrator/plans.jsonl` (rotated at 2 MB, keeping the
|
|
122
|
+
newest half) and prints one `[work-orchestrator] plan` line: the sessions sent with their availability, what was left
|
|
123
|
+
out and why, the usage snapshot's age per provider, the assignments (mode and target, never prompt text), the planner,
|
|
124
|
+
the outcome and the time taken.
|
|
125
|
+
|
|
126
|
+
## Control 0.5.333
|
|
127
|
+
|
|
128
|
+
- Plans with the server's input when `GET /api/work/chat` advertises `orchestrator: 1`; otherwise exactly as 0.5.332.
|
|
129
|
+
- Refreshes the session list right before planning. A plan from no sessions says why (none discovered; or N discovered
|
|
130
|
+
but none can take a Continue, each with its reason) and still lets the planner propose a new session.
|
|
131
|
+
- Journals `forkChildID` on fork receipts. Leaves Guardian review rows out.
|
|
132
|
+
- Auto-send: a per-card switch whose default is Settings › Work › Send plans without review (off by default). On: the
|
|
133
|
+
plan's assignments (Continue, Fork and New session) go through the existing handoff path without the review click,
|
|
134
|
+
each with a visible Stop and a log line. Off: today, unchanged.
|
|
135
|
+
|
|
136
|
+
## Lens plans (server 6.83.0, COS Control 0.5.333)
|
|
137
|
+
|
|
138
|
+
Contract: `operations/personal/wk43_2026/CONTRACT_lens_plans_6.83.0_2026-10-21.md` (MU repo), Miles 2026-10-21 09:11
|
|
139
|
+
("Add both"). The lens plans with `POST /api/work/chat` as above, then may save that plan into the card's Work chat
|
|
140
|
+
history and ask for it to be sent. COS Control stays the owner of every send and of its Work chat journal: the server
|
|
141
|
+
holds lens rounds and requests (`server/lib/work-lens-rounds.ts`, `server/routes/work-lens-rounds.ts`); Control pulls
|
|
142
|
+
them, writes the round into the card's record, sends through its own Send path and reports results.
|
|
143
|
+
|
|
144
|
+
| Who | Route | What |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| Lens | `POST /api/work/chat/lens-rounds` | Record a plan. 201 `{roundId, workID, state, control}`; a repeated `clientRoundId` 200, same body (answered from the store even if the card left the board). 400 `invalid_lens_round` + `field`, 404 `unknown_work`, 413 `lens_round_too_large`, 503 `work_board_unavailable` when neither Control's snapshot nor the board can say. |
|
|
147
|
+
| Lens | `GET /api/work/chat/lens-rounds/:roundId` | State (`recorded`, `in_history`, `send_requested`, `sending`, `sent`, `partly_sent`, `failed`, `stopped`, `refused`, `expired`), `inHistory`, `autoSendCard`, per assignment `{id, state, sessionID, receiptId, detail}`. Never prompts. |
|
|
148
|
+
| Lens | `POST /api/work/chat/lens-rounds/:roundId/send` | 202 `{sendRequestId, state: "send_requested"}`. Omitted ids: the whole plan minus what is already sent (or held by COS Control). 409 `send_in_progress`, `already_sent` (with `assignmentIds`: those already sent), `plan_stale`; 428 `confirmation_required` (no `confirm: "two-step"` and the card's Auto-send off); 423 `control_not_connected`. |
|
|
149
|
+
| Lens | `POST /api/work/chat/lens-rounds/:roundId/stop` | 202 `{stopRequestId, state: "stop_requested"}`; 423 without Control. |
|
|
150
|
+
| Control (loopback) | `GET /api/work/chat/lens-inbox?since=<cursor>&autoSend=0\|1` | Rounds not yet in history (after `since`, with their full plan), send requests (each with its round) and stop requests (with the send they belong to), 50 at most. Listing a send or stop is its pick-up: it is listed once. `autoSend` is Settings > Work > Send plans without review, the card-independent hint. |
|
|
151
|
+
| Control (loopback) | `POST /api/work/chat/lens-rounds/:roundId/history` | `{state: "in_history", autoSendCard?}` after Control wrote the round into the card's record. `autoSendCard` is the card's switch; Control re-posts it when the switch changes. |
|
|
152
|
+
| Control (loopback) | `POST /api/work/chat/lens-rounds/:roundId/mac-send` | The same `{assignments: [...]}` shape, when COS Control sent assignments of the round with its own Send on the Mac (after a 423 the wearer is told to send from the Mac). Those never go again from the lens. |
|
|
153
|
+
| Control (loopback) | `POST /api/work/chat/lens-send/:sendRequestId/result` | `{assignments: [{id, state, receiptId?, detail?, sessionID?}]}`, as often as states change (`held` until earlier work resolves, then `sent`; `stopped` after a Stop). |
|
|
154
|
+
|
|
155
|
+
Rules the server enforces:
|
|
156
|
+
|
|
157
|
+
- **Connected** means a loopback inbox poll in the last 60 s (Control polls every 5 s while running). A phone request
|
|
158
|
+
never counts. Without it a send or stop is 423 and the plan stays recorded, so it can be sent from the Mac.
|
|
159
|
+
- **Confirmation.** `confirm: "two-step"` (with `confirmedAt`) records the wearer's two-step tap. Without it the send is
|
|
160
|
+
refused 428 unless the card's Auto-send is on as Control last reported it; Control checks the switch again before it
|
|
161
|
+
sends and refuses an unconfirmed assignment if the switch has since gone off.
|
|
162
|
+
- **Plan stale.** At record time the server keeps the card's sessions from Control's snapshot (links, receipt targets,
|
|
163
|
+
fork children). A send is 409 `plan_stale` when that set changed (a session added or gone). Sessions the round's own
|
|
164
|
+
sends reported are not a change, so a partly sent plan can send the rest. No snapshot on either side: never stale.
|
|
165
|
+
- **Expiry.** A send request not picked up in 10 minutes is `expired` (and never handed to Control later); a new send
|
|
166
|
+
may be asked for. A picked-up assignment Control never reports is marked `failed` after 15 minutes so the lens can
|
|
167
|
+
stop polling. A round is kept 14 days; at most 200 are kept, oldest idle first.
|
|
168
|
+
- **Outcome.** Every assignment terminal (`sent`, `delivered`, `failed`, `refused`, `stopped`): all sent or delivered
|
|
169
|
+
is `sent`, some is `partly_sent`. None: all stopped is `stopped`; all refused (or refused and stopped) is `refused`, an
|
|
170
|
+
actual refusal (confirmation, a stale plan, a COS Control check); otherwise `failed`. The lens stops polling at
|
|
171
|
+
`sent`, `partly_sent`, `failed`, `stopped`, `refused` or `expired`.
|
|
172
|
+
- **Sent from the Mac.** An assignment COS Control reported through `mac-send` as `sent`, `delivered` or `held` (queued
|
|
173
|
+
behind earlier work) is already sent: naming it is 409 `already_sent` with its id, and an omitted list leaves it out.
|
|
174
|
+
The round reads as that Mac send's outcome (`partly_sent` while the rest of the plan was never sent).
|
|
175
|
+
- **Recording never sends.** COS Control writes a recorded round into history and nothing else; only a lens send request
|
|
176
|
+
(two-step confirm, or the card's Auto-send plus an explicit request) or Miles's own Send on the Mac sends it.
|
|
177
|
+
- **Maintenance.** Every route answers 503 `maintenance` while admissions are closed; writes hold the `api_mutation`
|
|
178
|
+
lease themselves (the global mutation lease skips `/work/chat/lens-*` so the code is the contract's).
|
|
179
|
+
- **Storage.** `data/work-lens-rounds.json`, 0600, atomic; an unreadable file is moved aside, never overwritten. Inside a
|
|
180
|
+
test the store is in memory only.
|
|
181
|
+
|
|
182
|
+
Checks: `server/routes/work-lens-rounds.test.ts` (record, idempotent repeat, send with and without confirm, Auto-send,
|
|
183
|
+
423, `plan_stale`, expiry, stop, loopback-only, maintenance, the existing GET and POST unchanged) and
|
|
184
|
+
`server/scripts/work-lens-rounds-mutation-gate.mjs` (not packaged).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gotcos/glasses-server",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.83.0",
|
|
4
4
|
"description": "COS Glasses: 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": {
|
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
"!server/scripts/fact-check-mutation-gate.mjs",
|
|
56
56
|
"!server/scripts/stale-session-mutation-gate.mjs",
|
|
57
57
|
"!server/scripts/autonomous-save-mutation-gate.mjs",
|
|
58
|
+
"!server/scripts/routines-mutation-gate.mjs",
|
|
59
|
+
"!server/scripts/routines-canary.ts",
|
|
58
60
|
"!server/scripts/stt-mutation-gate.mjs",
|
|
59
61
|
"!server/scripts/dictation-stt-mutation-gate.mjs",
|
|
60
62
|
"!server/scripts/pairing-mutation-gate.mjs",
|
|
@@ -86,6 +88,9 @@
|
|
|
86
88
|
"!server/scripts/dictation-polish-mutation-gate.mjs",
|
|
87
89
|
"!server/lib/meeting-engine/__fixtures__/**",
|
|
88
90
|
"!server/lib/__fixtures__/**",
|
|
91
|
+
"!server/lib/work-orchestrator/__fixtures__/**",
|
|
92
|
+
"!server/scripts/work-orchestrator-mutation-gate.mjs",
|
|
93
|
+
"docs/work-orchestrator.md",
|
|
89
94
|
"!shared/**/*.test.ts",
|
|
90
95
|
".env.example",
|
|
91
96
|
".cos-profile.example.json",
|
package/server/index.ts
CHANGED
|
@@ -158,6 +158,9 @@ import { createSubqKeyRouter } from './routes/subq-key.js'
|
|
|
158
158
|
import { createDecisionProviderRouter } from './routes/decision-provider.js'
|
|
159
159
|
import { createJevRouter } from './routes/jev.js'
|
|
160
160
|
import { createWorkChatRouter } from './routes/work-chat.js'
|
|
161
|
+
import { createWorkOrchestratorRouter } from './routes/work-orchestrator.js'
|
|
162
|
+
import { createWorkLensRoundsRouter } from './routes/work-lens-rounds.js'
|
|
163
|
+
import { startWorkOrchestrator } from './lib/work-orchestrator/runtime.js'
|
|
161
164
|
import { createWorkSearchRouter } from './routes/work-search.js'
|
|
162
165
|
import { isReadOnlyApiPost } from './lib/work-search.js'
|
|
163
166
|
import { dataPath } from './lib/data-dir.js'
|
|
@@ -170,6 +173,8 @@ import { listBoard } from './lib/task-store.js'
|
|
|
170
173
|
import { createDefaultWorkReviewRuntime } from './lib/work-review-backend.js'
|
|
171
174
|
import { createMorningBriefRouter } from './routes/morning-brief.js'
|
|
172
175
|
import { getMorningBriefScheduler, startMorningBriefScheduler, stopMorningBriefScheduler } from './lib/morning-brief-runtime.js'
|
|
176
|
+
import { createRoutinesRouter } from './routes/routines.js'
|
|
177
|
+
import { getRoutineRegistry, routineSkillCatalog, startRoutineScheduler, stopRoutineScheduler } from './lib/routines-runtime.js'
|
|
173
178
|
import {
|
|
174
179
|
initQueryJobRuntime,
|
|
175
180
|
preparePublicDurableQueryAdmission,
|
|
@@ -417,6 +422,8 @@ app.use('/api', (req, res, next) => {
|
|
|
417
422
|
|| req.path === '/meeting-ask/requests'
|
|
418
423
|
|| /^\/meeting-ask\/requests\/[A-Za-z0-9_-]+\/cancel$/.test(req.path)
|
|
419
424
|
|| req.path.startsWith('/maintenance/drain')
|
|
425
|
+
// 6.83.0 lens plans: each write takes the api_mutation lease itself and answers the contract's 503 `maintenance`.
|
|
426
|
+
|| req.path.startsWith('/work/chat/lens-')
|
|
420
427
|
if (lifecycleOwned) return next()
|
|
421
428
|
|
|
422
429
|
// COS Control must prove the candidate while a committed cross-boot gate is
|
|
@@ -834,10 +841,16 @@ app.use('/api', createDecisionProviderRouter())
|
|
|
834
841
|
app.use('/api', createSubqKeyRouter())
|
|
835
842
|
app.use('/api', createWorkSearchRouter())
|
|
836
843
|
app.use('/api', createWorkChatRouter())
|
|
844
|
+
// 6.83.0 Work orchestrator: Control's Work snapshot, the planner input and plan usage (docs/work-orchestrator.md).
|
|
845
|
+
app.use('/api', createWorkOrchestratorRouter())
|
|
846
|
+
// 6.83.0 lens plans: the lens records a plan and may ask for it to be sent; COS Control pulls, sends and reports.
|
|
847
|
+
app.use('/api', createWorkLensRoundsRouter())
|
|
837
848
|
app.use('/api', queryRouter)
|
|
838
849
|
// The scheduled start-of-day brief: settings, status, run-now. Same auth as
|
|
839
850
|
// every other settings route; the brief itself is an ordinary durable job.
|
|
840
851
|
app.use('/api', createMorningBriefRouter(getMorningBriefScheduler))
|
|
852
|
+
// 6.82.0: every routine (the brief first, then skill routines), each with its own cap, switch and breaker.
|
|
853
|
+
app.use('/api', createRoutinesRouter(getRoutineRegistry, routineSkillCatalog))
|
|
841
854
|
app.use('/api', providerProofRouter)
|
|
842
855
|
app.use('/api', transcribeRouter)
|
|
843
856
|
// Ported from cos-glasses-app in 6.24.0. The companion's Sessions tab has been
|
|
@@ -1257,6 +1270,7 @@ async function gracefulShutdown(): Promise<void> {
|
|
|
1257
1270
|
forceExit.unref?.()
|
|
1258
1271
|
await meetingAskRuntime.shutdown()
|
|
1259
1272
|
stopMorningBriefScheduler()
|
|
1273
|
+
stopRoutineScheduler()
|
|
1260
1274
|
await workReviewRuntime?.close()
|
|
1261
1275
|
workIntakeStore?.close()
|
|
1262
1276
|
workHandoffRequestStore?.close()
|
|
@@ -1477,6 +1491,9 @@ listenRequiredServers(listeners).then(() => {
|
|
|
1477
1491
|
resumeAutonomousMeetingSaves()
|
|
1478
1492
|
// 6.77.0: transcript cleanup data whose meeting is gone, and screen text past retention.
|
|
1479
1493
|
scheduleTranscriptCleanupSweep()
|
|
1494
|
+
// 6.83.0: the usage-event sink and a first background log scan. Plan usage is read on demand; the 10-minute poll
|
|
1495
|
+
// runs only with COS_WORK_USAGE_POLL=1.
|
|
1496
|
+
startWorkOrchestrator()
|
|
1480
1497
|
|
|
1481
1498
|
// Debris from an import killed mid-write: a temp file, or a sidecar whose
|
|
1482
1499
|
// markdown never landed. Both are invisible to every reader, so nothing
|
|
@@ -1534,6 +1551,8 @@ listenRequiredServers(listeners).then(() => {
|
|
|
1534
1551
|
// that store is ready. Its own tick checks the durable-jobs switch and the
|
|
1535
1552
|
// maintenance gate, so starting it here is safe when either is off.
|
|
1536
1553
|
startMorningBriefScheduler()
|
|
1554
|
+
// 6.82.0: skill routines ride the same coordinator and the same gates.
|
|
1555
|
+
startRoutineScheduler()
|
|
1537
1556
|
void workReviewRuntime?.start().catch(error => console.error('[work-reviews] reconciliation unavailable:', error))
|
|
1538
1557
|
}).catch(error => {
|
|
1539
1558
|
// The store remains degraded and rejects admission. Legacy /api/query is
|
|
@@ -60,6 +60,7 @@ import { terminalProviderAuthFailure } from './provider-terminal-error.js'
|
|
|
60
60
|
import { claudePermissionArgs, getClaudeTrustMode } from './claude-permissions.js'
|
|
61
61
|
import { terminateProviderProcess } from './provider-process-lifecycle.js'
|
|
62
62
|
import { teeJobTrail, type JobTrailDraft } from './job-trail.js'
|
|
63
|
+
import { noteClaudeRateLimitFrame } from './work-orchestrator/usage-events.js'
|
|
63
64
|
import { claudeSupportsSessionName } from './claude-cli-support.js'
|
|
64
65
|
import { applyTurnRequestEnv, createTurnRequestBinding, spawnWithTurnRequest } from './turn-request.js'
|
|
65
66
|
import { STEER_REAPER_MS, STEER_SETTLE_MS, SteerTracker, composeFirstContent, encodeUserLine, steerClock, type SteerPort } from './claude-steer.js'
|
|
@@ -436,6 +437,11 @@ export interface CallOptions {
|
|
|
436
437
|
dispatch?: { restricted: true; tools: readonly string[] }
|
|
437
438
|
/** 6.58.2: `claude --name` for a session this COS session starts fresh (QueryJobRequest.sessionName). */
|
|
438
439
|
sessionName?: string
|
|
440
|
+
/**
|
|
441
|
+
* 6.82.0: the token-audit `caller` for this run, so a server-started job is counted by who started
|
|
442
|
+
* it (`routine:morning-brief`, `routine:routine-<id>`). Absent, every bridge writes the caller it always did.
|
|
443
|
+
*/
|
|
444
|
+
auditCaller?: string
|
|
439
445
|
/**
|
|
440
446
|
* 6.78.0 Messages steer: the job's steer port. Present only when the coordinator decided the
|
|
441
447
|
* run is steerable (lib/claude-steer.ts). Absent, every argument and every stdin byte is the
|
|
@@ -927,7 +933,7 @@ export async function callClaudeStreaming(
|
|
|
927
933
|
inputChars: inputEstimate,
|
|
928
934
|
outputChars: text.length,
|
|
929
935
|
durationMs: totalMs,
|
|
930
|
-
caller: options?.lightweight ? 'voice_query' : 'full_query',
|
|
936
|
+
caller: options?.auditCaller ?? (options?.lightweight ? 'voice_query' : 'full_query'),
|
|
931
937
|
...keyAuditFields,
|
|
932
938
|
...keyUsdFields,
|
|
933
939
|
})
|
|
@@ -1129,6 +1135,9 @@ export async function callClaudeStreaming(
|
|
|
1129
1135
|
|
|
1130
1136
|
if (event.type === 'system' && event.subtype === 'init' && typeof event.model === 'string') {
|
|
1131
1137
|
updateClaudeRun(run.runId, { resolvedModelId: event.model })
|
|
1138
|
+
} else if (event.type === 'rate_limit_event') {
|
|
1139
|
+
// 6.83.0: free passive plan-usage refresh (canary C1). A key-lane turn's limits are the key's, never the plan's.
|
|
1140
|
+
if (!keyProfile) noteClaudeRateLimitFrame(event)
|
|
1132
1141
|
} else if (event.type === 'stream_event') {
|
|
1133
1142
|
// Real-time token streaming — fires every few tokens during generation
|
|
1134
1143
|
receivedStreamEvents = true
|
|
@@ -817,7 +817,7 @@ export async function callCodexStreaming(
|
|
|
817
817
|
inputChars: systemPrompt.length + fullQuery.length + contextPrompt.length,
|
|
818
818
|
outputChars: text.length,
|
|
819
819
|
durationMs: totalMs,
|
|
820
|
-
caller: options?.lightweight ? 'voice_query' : 'full_query',
|
|
820
|
+
caller: options?.auditCaller ?? (options?.lightweight ? 'voice_query' : 'full_query'),
|
|
821
821
|
...keyAuditFields,
|
|
822
822
|
...keyUsdFields,
|
|
823
823
|
})
|
|
@@ -528,7 +528,7 @@ export async function callCursorStreaming(
|
|
|
528
528
|
inputChars: systemPrompt.length + fullQuery.length + contextPrompt.length,
|
|
529
529
|
outputChars: text.length,
|
|
530
530
|
durationMs: totalMs,
|
|
531
|
-
caller: options?.lightweight ? 'voice_query' : 'full_query',
|
|
531
|
+
caller: options?.auditCaller ?? (options?.lightweight ? 'voice_query' : 'full_query'),
|
|
532
532
|
turnId: options?.turnId,
|
|
533
533
|
runId: run.runId,
|
|
534
534
|
sessionId: sid,
|
|
@@ -564,7 +564,7 @@ export async function callDirectStreaming(
|
|
|
564
564
|
inputChars: messages.reduce((n, m) => n + (typeof m.content === 'string' ? m.content.length : 0), 0),
|
|
565
565
|
outputChars: text.length,
|
|
566
566
|
durationMs: Date.now() - startTime,
|
|
567
|
-
caller: 'direct_query',
|
|
567
|
+
caller: options?.auditCaller ?? 'direct_query',
|
|
568
568
|
...(options?.turnId ? { turnId: options.turnId } : {}),
|
|
569
569
|
runId: run.runId,
|
|
570
570
|
sessionId: sid,
|
|
@@ -113,9 +113,15 @@ function daysBetween(fromDay: string, toDay: string): number {
|
|
|
113
113
|
return Math.round((Date.UTC(ty, tm - 1, td) - Date.UTC(fy, fm - 1, fd)) / 86_400_000)
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** The schedule fields the arithmetic reads. The morning brief config and a
|
|
117
|
+
* 6.82.0 skill routine both carry them, so both share one decision table. */
|
|
118
|
+
export type ScheduleShape = Pick<MorningBriefConfig, 'enabled' | 'time' | 'timezone' | 'days' | 'catchUpMinutes'>
|
|
119
|
+
/** The ledger fields the fire decision reads (a brief run and a routine run both have them). */
|
|
120
|
+
export type ScheduledRunShape = Pick<MorningBriefRun, 'day' | 'trigger' | 'attempt' | 'firedAt' | 'jobId' | 'submitError'>
|
|
121
|
+
|
|
116
122
|
/** ISO instant of the next scheduled fire at or after `nowMs`, or null when
|
|
117
123
|
* the brief is disabled or no weekday is selected. */
|
|
118
|
-
export function nextScheduledFire(config:
|
|
124
|
+
export function nextScheduledFire(config: Pick<ScheduleShape, 'enabled' | 'time' | 'timezone' | 'days'>, nowMs: number): string | null {
|
|
119
125
|
if (!config.enabled || config.days.length === 0) return null
|
|
120
126
|
const target = parseTime(config.time)
|
|
121
127
|
const clock = localClock(nowMs, config.timezone)
|
|
@@ -129,8 +135,8 @@ export function nextScheduledFire(config: MorningBriefConfig, nowMs: number): st
|
|
|
129
135
|
return null
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
export type FireDecision =
|
|
133
|
-
| { fire: true; day: string; attempt: number; resume?:
|
|
138
|
+
export type FireDecision<R extends ScheduledRunShape = MorningBriefRun> =
|
|
139
|
+
| { fire: true; day: string; attempt: number; resume?: R }
|
|
134
140
|
| { fire: false; reason: 'disabled' | 'not_a_brief_day' | 'before_slot' | 'window_closed' | 'already_fired' | 'in_flight' | 'attempts_exhausted' | 'backoff' }
|
|
135
141
|
|
|
136
142
|
/**
|
|
@@ -140,11 +146,11 @@ export type FireDecision =
|
|
|
140
146
|
* job id and no error is a crash between ledger write and admission — resumed
|
|
141
147
|
* by client identity rather than re-run.
|
|
142
148
|
*/
|
|
143
|
-
export function decideScheduledFire(
|
|
144
|
-
config:
|
|
145
|
-
runs: readonly
|
|
149
|
+
export function decideScheduledFire<R extends ScheduledRunShape = MorningBriefRun>(
|
|
150
|
+
config: ScheduleShape,
|
|
151
|
+
runs: readonly R[],
|
|
146
152
|
nowMs: number,
|
|
147
|
-
): FireDecision {
|
|
153
|
+
): FireDecision<R> {
|
|
148
154
|
if (!config.enabled) return { fire: false, reason: 'disabled' }
|
|
149
155
|
const clock = localClock(nowMs, config.timezone)
|
|
150
156
|
if (!config.days.includes(clock.weekday)) return { fire: false, reason: 'not_a_brief_day' }
|
|
@@ -37,6 +37,7 @@ import type { MaintenanceWorkLease } from './maintenance-lifecycle.js'
|
|
|
37
37
|
import { applyTrailToolMode, type JobTrailDraft } from './job-trail.js'
|
|
38
38
|
import { crashLoopParkedError, type BootResumeGate } from './boot-resume.js'
|
|
39
39
|
import { restartNoticeEnabled } from './query-job-feature.js'
|
|
40
|
+
import { noteJobProviderLimit } from './work-orchestrator/usage-events.js'
|
|
40
41
|
|
|
41
42
|
const CLIENT_JOB_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
42
43
|
|
|
@@ -836,7 +837,10 @@ export class QueryJobCoordinator {
|
|
|
836
837
|
this.finishActive(active)
|
|
837
838
|
return false
|
|
838
839
|
}
|
|
839
|
-
const
|
|
840
|
+
const failure = normalizeQueryJobError(error)
|
|
841
|
+
// 6.83.0: a plan-lane provider limit marks that provider exhausted for the Work orchestrator (until its reset).
|
|
842
|
+
noteJobProviderLimit(snapshot.provider, failure.code, error instanceof Error ? error.message : String((error as any)?.message ?? ''), active.request.model, snapshot.lane)
|
|
843
|
+
const result = await this.store.fail(active.jobId, failure)
|
|
840
844
|
const projected = isTerminalQueryJobStatus(result.job.status)
|
|
841
845
|
? await this.projectTerminal(result.job, active.request)
|
|
842
846
|
: false
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { routineAuditCaller } from './routines-config.js'
|
|
1
2
|
import { carriesBoundTo } from './agent-session-binding-store.js'
|
|
2
3
|
import { randomUUID } from 'node:crypto'
|
|
3
4
|
import { resolve } from 'node:path'
|
|
@@ -420,6 +421,8 @@ const runner: QueryJobRunner = async ({ jobId, turnId, request, signal, callback
|
|
|
420
421
|
sessionLockHeld: true,
|
|
421
422
|
...(request.dispatch ? { dispatch: request.dispatch } : {}),
|
|
422
423
|
...(request.sessionName ? { sessionName: request.sessionName } : {}),
|
|
424
|
+
// 6.82.0: a routine's run is counted in the token audit under the routine that started it.
|
|
425
|
+
...(routineAuditCaller(request) ? { auditCaller: routineAuditCaller(request) } : {}),
|
|
423
426
|
...(request.keyTurnOnly ? { keyTurnOnly: true } : {}),
|
|
424
427
|
// 6.78.0: only on a run the coordinator decided is steerable.
|
|
425
428
|
...(steer ? { steer } : {}),
|