@gotcos/glasses-server 6.99.29 → 7.0.7
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/.env.example +18 -0
- package/CHANGELOG.md +475 -0
- package/README.md +3 -1
- package/managed-runtime-contract.json +3 -0
- package/package.json +1 -1
- package/server/index.ts +29 -7
- package/server/lib/agent-session-output-images.ts +627 -49
- package/server/lib/agent-session-store.ts +24 -11
- package/server/lib/agent-session-tool-images.ts +224 -0
- package/server/lib/agent-session-turns.ts +19 -2
- package/server/lib/api-auth.ts +6 -0
- package/server/lib/ask-work-plan.ts +3 -1
- package/server/lib/calendar-source.ts +491 -0
- package/server/lib/codex-bridge.ts +4 -1
- package/server/lib/codex-run-ledger.ts +29 -0
- package/server/lib/context-builder.ts +2 -1
- package/server/lib/drop-p2-runtime.ts +1 -1
- package/server/lib/drop-store.ts +10 -4
- package/server/lib/learning-needs-you.ts +24 -7
- package/server/lib/live-events.ts +533 -0
- package/server/lib/local-path-redact.ts +272 -0
- package/server/lib/meeting-ask-orientation.ts +40 -6
- package/server/lib/meeting-ask-runner.ts +30 -10
- package/server/lib/morning-brief-runtime.ts +4 -2
- package/server/lib/native-mac-caller.ts +29 -0
- package/server/lib/ping-engine.ts +1 -1
- package/server/lib/ping-sources.ts +11 -6
- package/server/lib/preview-content.ts +172 -0
- package/server/lib/preview-error-code.ts +10 -0
- package/server/lib/preview-folders-setting.ts +133 -0
- package/server/lib/preview-log-throttle.ts +18 -0
- package/server/lib/preview-thumbs.ts +139 -0
- package/server/lib/secret-path-guard.ts +80 -0
- package/server/lib/session-files-setting.ts +88 -0
- package/server/lib/session-links.ts +280 -0
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +119 -28
- package/server/lib/web-memories-bridge.ts +3 -1
- package/server/lib/work-activity.ts +56 -6
- package/server/lib/work-session-activity.ts +260 -0
- package/server/lib/your-move-count.ts +9 -3
- package/server/routes/agent-session-bindings.ts +13 -0
- package/server/routes/agent-session-stream.ts +16 -0
- package/server/routes/agent-sessions.ts +146 -11
- package/server/routes/ask-work-plan.ts +4 -1
- package/server/routes/claude-sessions.ts +5 -1
- package/server/routes/health.ts +35 -0
- package/server/routes/live-events.ts +329 -0
- package/server/routes/permission-broker.ts +69 -5
- package/server/routes/session-preview.ts +200 -0
- package/server/routes/thread-turn-queue.ts +207 -3
- package/server/routes/today-brief.ts +44 -9
- package/server/routes/welcome-context.ts +23 -3
- package/server/routes/work-board.ts +9 -2
- package/server/web-control/app.css +79 -9
- package/server/web-control/app.js +1543 -65
- package/server/web-control/control.css +37 -6
- package/server/web-control/index.html +8 -2
- package/server/web-control/memory-review.js +100 -0
- package/server/web-control/panes/cos-chat.css +51 -6
- package/server/web-control/panes/cos-chat.js +268 -16
- package/server/web-control/panes/meetings.css +23 -4
- package/server/web-control/panes/meetings.js +25 -9
- package/server/web-control/panes/memories.css +18 -3
- package/server/web-control/panes/memories.js +130 -50
- package/server/web-control/panes/messages.js +21 -2
- package/server/web-control/panes/preview.js +318 -0
- package/server/web-control/panes/questions.css +20 -4
- package/server/web-control/panes/questions.js +127 -26
- package/server/web-control/panes/reader-media.js +409 -23
- package/server/web-control/panes/sessions.css +50 -10
- package/server/web-control/panes/settings.js +96 -5
- package/server/web-control/panes/speakers.css +3 -5
- package/server/web-control/panes/speakers.js +7 -4
- package/server/web-control/panes/threads.js +3 -2
- package/server/web-control/panes/today-actions.css +49 -18
- package/server/web-control/panes/today-actions.js +217 -50
- package/server/web-control/panes/work.css +11 -1
- package/server/web-control/panes/work.js +10 -2
- package/server/web-control/shell.js +20 -14
|
@@ -79,13 +79,22 @@ import {
|
|
|
79
79
|
} from '../lib/occupancy-probes.js'
|
|
80
80
|
import type { OccupancyDirs } from '../lib/thread-occupancy.js'
|
|
81
81
|
import { parseTurnsParam, readRecentSessionTurns, type RecentTurnsRead } from '../lib/agent-session-turns.js'
|
|
82
|
+
import { redactLocalPathsDeep } from '../lib/local-path-redact.js'
|
|
83
|
+
import { isNativeMacCaller } from '../lib/native-mac-caller.js'
|
|
82
84
|
import { cosSpawnedPids } from '../lib/agent-session-ownership-store.js'
|
|
83
85
|
import { sessionActFactsFor, sessionActFields } from '../lib/session-act-fields.js'
|
|
84
86
|
import { CONTINUE_NOTE_HEADER, continueNoteAcknowledged } from '../lib/continue-plan.js'
|
|
85
87
|
|
|
86
|
-
import { agentSessionOutputImages } from '../lib/agent-session-output-images.js'
|
|
88
|
+
import { agentSessionOutputImages, sharedFileHeaders } from '../lib/agent-session-output-images.js'
|
|
89
|
+
import { previewErrorCode } from '../lib/preview-error-code.js'
|
|
90
|
+
import { resetPreviewLogThrottle, warnPreviewThrottled } from '../lib/preview-log-throttle.js'
|
|
91
|
+
import { sessionFilesSetting, withoutPreviews } from '../lib/session-files-setting.js'
|
|
92
|
+
import { withoutLinkMarkers } from '../lib/session-links.js'
|
|
93
|
+
import { createSessionPreviewRouter } from './session-preview.js'
|
|
87
94
|
|
|
88
95
|
export const agentSessionsRouter = Router()
|
|
96
|
+
// Preview engine phase 1: GET /api/preview/agent-sessions/... and /api/preview-folders/settings (routes/session-preview.ts).
|
|
97
|
+
agentSessionsRouter.use(createSessionPreviewRouter())
|
|
89
98
|
|
|
90
99
|
function boundedInteger(value: unknown, fallback: number, min: number, max: number): number {
|
|
91
100
|
const parsed = Number(value)
|
|
@@ -567,7 +576,8 @@ agentSessionsRouter.get('/agent-sessions', async (req, res) => {
|
|
|
567
576
|
const queuedOf = await allQueuedWaitingLookup(now)
|
|
568
577
|
const codexRoots = codexSessionRootsOf(roots)
|
|
569
578
|
const subagents = await readCodexSubagentsAcross(sessions.filter(row => row.provider === 'codex').map(row => row.session_id), codexRoots, now, codexRoots.length > 1 ? keyedCodexRootOf() : undefined)
|
|
570
|
-
|
|
579
|
+
// 7.0.4 S1: no absolute Mac path in any string a list row carries (titles, first prompts, summaries).
|
|
580
|
+
res.json(redactLocalPathsDeep({
|
|
571
581
|
sessions: sessions.map((row, index) => {
|
|
572
582
|
const derived = derivedById.get(row.session_id)
|
|
573
583
|
const signal = signalFor(row.session_id)
|
|
@@ -592,7 +602,7 @@ agentSessionsRouter.get('/agent-sessions', async (req, res) => {
|
|
|
592
602
|
// client ignores this key. Zero means the walk found nothing to hide, not
|
|
593
603
|
// that the caps are off.
|
|
594
604
|
dropped,
|
|
595
|
-
})
|
|
605
|
+
}))
|
|
596
606
|
} catch (error) {
|
|
597
607
|
console.error(`[agent-sessions] list failed: ${error instanceof Error ? error.message : error}`)
|
|
598
608
|
res.status(500).json({ error: 'Failed to read agent sessions', reason: 'agent_sessions_read_failed' })
|
|
@@ -631,30 +641,59 @@ agentSessionsRouter.get('/agent-sessions/search', async (req, res) => {
|
|
|
631
641
|
remember: false,
|
|
632
642
|
})
|
|
633
643
|
}
|
|
634
|
-
|
|
644
|
+
// 7.0.4 S1: search hits carry snippets of the same words.
|
|
645
|
+
res.json(redactLocalPathsDeep({
|
|
635
646
|
...result,
|
|
636
647
|
hits: result.hits.map(hit => toSearchHit(hit, queuedOf(hit.provider, hit.session_id), derivedFor(hit))),
|
|
637
|
-
})
|
|
648
|
+
}))
|
|
638
649
|
} catch (error) {
|
|
639
650
|
console.error(`[agent-sessions] search failed: ${error instanceof Error ? error.message : error}`)
|
|
640
651
|
res.status(500).json({ error: 'Failed to search agent sessions', reason: 'agent_sessions_search_failed' })
|
|
641
652
|
}
|
|
642
653
|
})
|
|
643
654
|
|
|
655
|
+
/** 7.0.4 S2: the 403 every local-file route answers for a file whose name says it may hold secrets. */
|
|
656
|
+
export const SECRET_FILE_DENIED = 'secret_file_denied'
|
|
657
|
+
/** 7.0.4 QA: the 503 a preview answers when the Mac is already reading its limit of previews (not unreachable). */
|
|
658
|
+
export const PREVIEWS_BUSY = 'previews_busy'
|
|
659
|
+
|
|
660
|
+
type PreviewKind = 'output_image' | 'tool_image' | 'sent_file'
|
|
661
|
+
/**
|
|
662
|
+
* 7.0.4 QA: every preview refusal and failure is logged with its kind and a reason code, never a path or a session's
|
|
663
|
+
* words. A burst of the same reason (a page of thumbnails while the reader is full) logs once per 10 seconds, with the
|
|
664
|
+
* count it held back (lib/preview-log-throttle.ts, shared with the preview engine routes since 7.0.7).
|
|
665
|
+
*/
|
|
666
|
+
export function logPreviewOutcome(kind: PreviewKind, reason: string, now = Date.now()): void {
|
|
667
|
+
warnPreviewThrottled(kind + ':' + reason, `[agent-sessions] preview ${kind} refused reason=${reason}`, now)
|
|
668
|
+
}
|
|
669
|
+
/** Tests only: forget the burst window, so each case logs. */
|
|
670
|
+
export { resetPreviewLogThrottle }
|
|
671
|
+
/** The answer for a preview read that did not succeed, logged (a missing id is ordinary and is not). */
|
|
672
|
+
function previewRefusal(res: import('express').Response, kind: PreviewKind, status: 'not_found' | 'unavailable' | 'too_large' | 'secret' | 'busy'): void {
|
|
673
|
+
const noun = kind === 'sent_file' ? 'file' : 'image'
|
|
674
|
+
if (status !== 'not_found') logPreviewOutcome(kind, status === 'secret' ? SECRET_FILE_DENIED : status)
|
|
675
|
+
if (status === 'secret') { res.status(403).json({ error: SECRET_FILE_DENIED }); return }
|
|
676
|
+
if (status === 'not_found') { res.status(404).json({ error: `${noun}_not_found` }); return }
|
|
677
|
+
if (status === 'too_large') { res.status(413).json({ error: `${noun}_too_large` }); return }
|
|
678
|
+
if (status === 'busy') { res.set('Retry-After', '2'); res.status(503).json({ error: PREVIEWS_BUSY }); return }
|
|
679
|
+
res.status(503).json({ error: `${noun}_unavailable` })
|
|
680
|
+
}
|
|
681
|
+
|
|
644
682
|
// Authenticated by index.ts's shared /api gate. Neither the request nor the response
|
|
645
683
|
// carries a filesystem path; the resolver reauthorizes exact transcript membership.
|
|
646
684
|
agentSessionsRouter.get('/agent-sessions/:provider/:sessionId/output-images/:imageId/content', async (req, res) => {
|
|
647
685
|
res.set('Cache-Control', 'private, no-store')
|
|
648
686
|
res.set('X-Content-Type-Options', 'nosniff')
|
|
687
|
+
// 6.99.35: "Show files and images your sessions make" off answers before anything is read.
|
|
688
|
+
if (!sessionFilesSetting.read().enabled) { res.status(403).json({ error: 'previews_off' }); return }
|
|
649
689
|
const provider = asProvider(String(req.params.provider ?? '').toLowerCase())
|
|
650
690
|
if (!provider || (req.query.variant !== undefined && req.query.variant !== 'thumb' && req.query.variant !== 'phone')) {
|
|
651
691
|
res.status(404).json({ error: 'image_not_found' }); return
|
|
652
692
|
}
|
|
653
693
|
try {
|
|
654
694
|
const content = await agentSessionOutputImages.content(provider, String(req.params.sessionId), String(req.params.imageId), req.query.variant === 'thumb' ? 'thumb' : 'phone')
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
}
|
|
695
|
+
// 7.0.4 S2: the id was authorized, and the file's name says it may hold secrets (lib/secret-path-guard.ts).
|
|
696
|
+
if (content.status !== 'ok') { previewRefusal(res, 'output_image', content.status === 'expired' ? 'unavailable' : content.status); return }
|
|
658
697
|
res.type('image/jpeg')
|
|
659
698
|
// `send` defaults to `dotfiles: 'ignore'` and applies it to the WHOLE absolute path
|
|
660
699
|
// when no `root` is given, so a media root under `~/.cos-glasses/data` 404'd on
|
|
@@ -666,8 +705,77 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId/output-images/:ima
|
|
|
666
705
|
if (!res.headersSent) res.status(503).json({ error: 'image_unavailable' })
|
|
667
706
|
else res.destroy()
|
|
668
707
|
})
|
|
669
|
-
} catch {
|
|
670
|
-
|
|
708
|
+
} catch (error) {
|
|
709
|
+
logPreviewOutcome('output_image', 'error_' + previewErrorCode(error))
|
|
710
|
+
res.status(503).json({ error: 'image_unavailable' })
|
|
711
|
+
}
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
// 6.99.35: "Show files and images your sessions make" (lib/session-files-setting.ts), Control Web Settings, Privacy.
|
|
715
|
+
// Default on; COS_WEB_SESSION_FILES=0 on the Mac forces it off and the answer says so (source: 'env').
|
|
716
|
+
agentSessionsRouter.get('/session-files/settings', (_req, res) => {
|
|
717
|
+
res.set('Cache-Control', 'private, no-store')
|
|
718
|
+
try { res.json(sessionFilesSetting.read()) } catch { res.status(500).json({ error: 'session_files_settings_unavailable' }) }
|
|
719
|
+
})
|
|
720
|
+
agentSessionsRouter.post('/session-files/settings', (req, res) => {
|
|
721
|
+
const body = req.body
|
|
722
|
+
if (!body || typeof body !== 'object' || Array.isArray(body) || Object.keys(body).some(k => k !== 'enabled') || typeof body.enabled !== 'boolean') {
|
|
723
|
+
res.status(400).json({ error: { code: 'invalid_session_files_settings', message: 'Send { enabled: true } or { enabled: false }.' } }); return
|
|
724
|
+
}
|
|
725
|
+
try { res.json(sessionFilesSetting.set(body.enabled)) } catch { res.status(500).json({ error: 'session_files_settings_unavailable' }) }
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
// 6.99.35: a file a Claude session sent with SendUserFile. Same gate, same re-authorization, no path
|
|
729
|
+
// in the request or the response. HTML is an attachment of type octet-stream with a sandbox CSP, never
|
|
730
|
+
// same-origin HTML; a PDF may open inline.
|
|
731
|
+
agentSessionsRouter.get('/agent-sessions/:provider/:sessionId/output-files/:fileId/content', async (req, res) => {
|
|
732
|
+
res.set('Cache-Control', 'private, no-store')
|
|
733
|
+
res.set('X-Content-Type-Options', 'nosniff')
|
|
734
|
+
if (!sessionFilesSetting.read().enabled) { res.status(403).json({ error: 'previews_off' }); return }
|
|
735
|
+
const provider = asProvider(String(req.params.provider ?? '').toLowerCase())
|
|
736
|
+
if (!provider) { res.status(404).json({ error: 'file_not_found' }); return }
|
|
737
|
+
try {
|
|
738
|
+
const content = await agentSessionOutputImages.fileContent(provider, String(req.params.sessionId), String(req.params.fileId))
|
|
739
|
+
if (content.status !== 'ok') { previewRefusal(res, 'sent_file', content.status); return }
|
|
740
|
+
res.set(sharedFileHeaders(content.name, content.kind))
|
|
741
|
+
res.status(200).end(content.bytes)
|
|
742
|
+
} catch (error) {
|
|
743
|
+
logPreviewOutcome('sent_file', 'error_' + previewErrorCode(error))
|
|
744
|
+
res.status(503).json({ error: 'file_unavailable' })
|
|
745
|
+
}
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
// Images a session looked at (tool results such as a Read of a PNG or a browser screenshot) and images attached
|
|
749
|
+
// to its prompts. Same gate and switch as output images, same rule: no path in the request or the response. Embedded
|
|
750
|
+
// bytes are served as the image type they ARE (png, jpeg, webp or gif, sniffed, never the declared type), with a
|
|
751
|
+
// sandbox CSP; a path-only Read comes back through the output-image normalizer as a JPEG.
|
|
752
|
+
export const TOOL_IMAGE_CSP = "sandbox; default-src 'none'"
|
|
753
|
+
agentSessionsRouter.get('/agent-sessions/:provider/:sessionId/tool-images/:imageId/content', async (req, res) => {
|
|
754
|
+
res.set('Cache-Control', 'private, no-store')
|
|
755
|
+
res.set('X-Content-Type-Options', 'nosniff')
|
|
756
|
+
if (!sessionFilesSetting.read().enabled) { res.status(403).json({ error: 'previews_off' }); return }
|
|
757
|
+
const provider = asProvider(String(req.params.provider ?? '').toLowerCase())
|
|
758
|
+
if (!provider) { res.status(404).json({ error: 'image_not_found' }); return }
|
|
759
|
+
try {
|
|
760
|
+
const content = await agentSessionOutputImages.toolImageContent(provider, String(req.params.sessionId), String(req.params.imageId))
|
|
761
|
+
if (content.status !== 'ok') { previewRefusal(res, 'tool_image', content.status); return }
|
|
762
|
+
res.set('Content-Security-Policy', TOOL_IMAGE_CSP)
|
|
763
|
+
res.set('Cross-Origin-Resource-Policy', 'same-origin')
|
|
764
|
+
if ('path' in content) {
|
|
765
|
+
res.type('image/jpeg')
|
|
766
|
+
// dotfiles 'allow': the media root lives under ~/.cos-glasses/data (see the output-images route above).
|
|
767
|
+
res.sendFile(content.path, { dotfiles: 'allow' }, err => {
|
|
768
|
+
if (!err) return
|
|
769
|
+
if (!res.headersSent) res.status(503).json({ error: 'image_unavailable' })
|
|
770
|
+
else res.destroy()
|
|
771
|
+
})
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
res.set('Content-Type', content.type)
|
|
775
|
+
res.set('Content-Disposition', 'inline')
|
|
776
|
+
res.status(200).end(content.bytes)
|
|
777
|
+
} catch (error) {
|
|
778
|
+
logPreviewOutcome('tool_image', 'error_' + previewErrorCode(error))
|
|
671
779
|
res.status(503).json({ error: 'image_unavailable' })
|
|
672
780
|
}
|
|
673
781
|
})
|
|
@@ -717,6 +825,8 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
|
|
|
717
825
|
try {
|
|
718
826
|
const images = await agentSessionOutputImages.context(provider, sessionId, found)
|
|
719
827
|
recentTurns = await readRecentSessionTurns(provider, found, turnsWanted, undefined, images.project)
|
|
828
|
+
// 6.99.35: previews off sends one mark per message instead of image and file names.
|
|
829
|
+
if (recentTurns && !sessionFilesSetting.read().enabled) recentTurns = { ...recentTurns, turns: withoutPreviews(recentTurns.turns) }
|
|
720
830
|
} catch (error) {
|
|
721
831
|
console.warn(`[agent-sessions] recent turns failed provider=${provider}: ${error instanceof Error ? error.message : error}`)
|
|
722
832
|
}
|
|
@@ -754,7 +864,10 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
|
|
|
754
864
|
}
|
|
755
865
|
const signal = signalFor(parsed.session_id)
|
|
756
866
|
const hint = runningHintFor(provider, derived, hintClock(signal, derived, rollout), now, activityClockMs(st.mtimeMs, activity))
|
|
757
|
-
|
|
867
|
+
// 7.0.4 S1: every string the detail sends (the reply, the digest, the first prompt, each turn's words, tool
|
|
868
|
+
// labels, file and image names) has its absolute local paths turned into base names (lib/local-path-redact.ts).
|
|
869
|
+
// A walk over the whole value rather than a field list, so a field added later is covered too.
|
|
870
|
+
const body = redactLocalPathsDeep({
|
|
758
871
|
...withRunning({ session_id: parsed.session_id }, running, hint),
|
|
759
872
|
...derivedRowFields(derived),
|
|
760
873
|
...queuedTurnsFields((await allQueuedWaitingLookup(Date.now()))(provider, parsed.session_id)),
|
|
@@ -816,6 +929,28 @@ agentSessionsRouter.get('/agent-sessions/:provider/:sessionId', async (req, res)
|
|
|
816
929
|
noteAck: continueNoteAcknowledged(req.headers[CONTINUE_NOTE_HEADER]),
|
|
817
930
|
}))[0]),
|
|
818
931
|
})
|
|
932
|
+
// 7.0.4 W1: COS Control's helper on this Mac matches a Work prompt it sent against the session's user turns by
|
|
933
|
+
// their opening words, and it holds the prompt unredacted. Only the user turns' words in `recent_turns` (the one
|
|
934
|
+
// field it matches on) are sent as written, and only to that privileged local caller: a direct loopback request
|
|
935
|
+
// that passed the header-token gate with no browser or proxy header (lib/native-mac-caller.ts). Every browser
|
|
936
|
+
// (cookie or token), phone, LAN, tailnet and proxied request gets the redacted body. The answer is already
|
|
937
|
+
// `private, no-store`; Vary names every header the choice reads, so no cache can serve one as the other.
|
|
938
|
+
if (turnsWanted !== null) res.vary('Cookie').vary('X-COS-Token').vary('Origin').vary('Referer').vary('User-Agent').vary('Sec-Fetch-Mode').vary('Sec-Fetch-Site').vary('Sec-Fetch-Dest')
|
|
939
|
+
// 7.0.7 final QA: link-card markers (`[words](cos-file:N)`) go only to a client that asks for them with `cards=1`
|
|
940
|
+
// (Control Web from 7.0.7: the reader and Today's session sheet). Everyone else gets each card's words, as 7.0.4
|
|
941
|
+
// showed them: a Control Web tab opened before the update (it has no reload-on-version), COS Control (no build
|
|
942
|
+
// renders markers; its own cards come from outputLinks), the lens and any older client. outputLinks is unchanged.
|
|
943
|
+
const native = recentTurns && Array.isArray(body.recent_turns) ? isNativeMacCaller(req, res) : false
|
|
944
|
+
const cardsWanted = req.query.cards === '1'
|
|
945
|
+
// The native helper always gets words, whatever it asks for (no Control build draws markers).
|
|
946
|
+
if (recentTurns && Array.isArray(body.recent_turns) && (native || !cardsWanted)) {
|
|
947
|
+
body.recent_turns = body.recent_turns.map((turn, index) => {
|
|
948
|
+
const original = recentTurns!.turns[index]
|
|
949
|
+
if (native && original && original.role === 'user' && turn.role === 'user') return { ...turn, text: original.text }
|
|
950
|
+
return typeof turn.text === 'string' && turn.text.includes('](cos-file:') ? { ...turn, text: withoutLinkMarkers(turn.text) } : turn
|
|
951
|
+
})
|
|
952
|
+
}
|
|
953
|
+
res.json(body)
|
|
819
954
|
} catch (error) {
|
|
820
955
|
console.error(`[agent-sessions] detail failed: ${error instanceof Error ? error.message : error}`)
|
|
821
956
|
res.status(500).json({ error: 'Failed to read agent session', reason: 'agent_sessions_read_failed' })
|
|
@@ -99,8 +99,11 @@ export function createAskWorkPlanRouter(given: Partial<AskWorkPlanDeps> = {}) {
|
|
|
99
99
|
model: output.model ?? `claude:${planner} (requested)`, effort })
|
|
100
100
|
} catch (error) {
|
|
101
101
|
if (error instanceof AskProcessError && error.terminationUnconfirmed) {
|
|
102
|
-
//
|
|
102
|
+
// No second process or maintenance drain until the runner proves that the
|
|
103
|
+
// original child and group have terminated. Missing/failed proof stays held.
|
|
103
104
|
release = false
|
|
105
|
+
const heldLease = lease
|
|
106
|
+
void error.terminationConfirmed?.then(() => { heldLease?.release(); running = false }).catch(() => { /* remain fail-closed */ })
|
|
104
107
|
console.warn('[ask-work-plan] provider termination unconfirmed; admission and maintenance lease retained')
|
|
105
108
|
fail(new AskWorkPlanError('provider_termination_unconfirmed', 503))
|
|
106
109
|
} else fail(error)
|
|
@@ -95,11 +95,15 @@ export async function readClaudePeerRecords(
|
|
|
95
95
|
dir: string,
|
|
96
96
|
probes: PeerProbes = realProbes,
|
|
97
97
|
showNames = claudeSessionNamesVisible(),
|
|
98
|
+
/** 7.0.1 live events: a folder that exists but cannot be read (permissions, a transient error) throws instead of
|
|
99
|
+
* answering "nothing alive", so the caller keeps its last good reading. A missing folder is still empty. */
|
|
100
|
+
throwWhenUnreadable = false,
|
|
98
101
|
): Promise<ClaudePeerRecord[]> {
|
|
99
102
|
let names: string[]
|
|
100
103
|
try {
|
|
101
104
|
names = await readdir(dir)
|
|
102
|
-
} catch {
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (throwWhenUnreadable && (error as NodeJS.ErrnoException)?.code !== 'ENOENT') throw error
|
|
103
107
|
// No directory means Claude Code has never run here, or the path is wrong.
|
|
104
108
|
// An empty presence list is the honest answer; the route reports `enabled`
|
|
105
109
|
// separately so this cannot be mistaken for "the feature is off".
|
package/server/routes/health.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MAX_ATTACHMENTS_PER_PROMPT } from '../../shared/media-attachment.js'
|
|
2
|
+
import { calendarStatus } from '../lib/calendar-source.js'
|
|
2
3
|
import { TURN_FILE_MAX_BYTES } from '../lib/turn-file-store.js'
|
|
3
4
|
import { dropItCapability } from '../lib/drop-settings.js'
|
|
4
5
|
import { PREP_CAPABILITY } from '../../shared/prep.js'
|
|
@@ -56,6 +57,10 @@ import {
|
|
|
56
57
|
isOllamaProviderReady,
|
|
57
58
|
} from '../lib/ollama-catalog.js'
|
|
58
59
|
import { isMediaProcessingReady } from '../lib/image-safety.js'
|
|
60
|
+
import { DEFAULT_MODEL, type ModelPreference } from '../../shared/model-preference.js'
|
|
61
|
+
import { closedEnvDefaultModel } from '../lib/key-profile-routing.js'
|
|
62
|
+
import { getSessionBinding, getSessionModel } from '../lib/conversation.js'
|
|
63
|
+
import { isProfilePreference } from '../../shared/key-preference.js'
|
|
59
64
|
import {
|
|
60
65
|
MAX_OTHER_MEDIA_BYTES,
|
|
61
66
|
MAX_VIDEO_MEDIA_BYTES,
|
|
@@ -625,6 +630,32 @@ function crashLoopParkingHealth(): { enabled: boolean; parked: number } {
|
|
|
625
630
|
}
|
|
626
631
|
}
|
|
627
632
|
|
|
633
|
+
/**
|
|
634
|
+
* 7.0.2 (A7): what "Default" in a model picker resolves to, in model-router's own order
|
|
635
|
+
* (lib/model-router.ts, the resolvedModel line): the request's model, then the session's
|
|
636
|
+
* preference, then COS_G2_DEFAULT_MODEL, then DEFAULT_MODEL. A picker on "Default" sends no
|
|
637
|
+
* model, so the request step only names a model a caller passes in. `source` says which step won.
|
|
638
|
+
* A slot name only: never a key profile (closedEnvDefaultModel and getSessionModel are closed).
|
|
639
|
+
*/
|
|
640
|
+
export type DefaultModelSource = 'request' | 'session' | 'profile' | 'env' | 'built-in'
|
|
641
|
+
/** `preference` is null only for `profile`: the session is bound to a key profile, never named here (plan 5.2). */
|
|
642
|
+
export function resolveDefaultModel(input: { requested?: ModelPreference | null; sessionModel?: ModelPreference | null; sessionProfile?: boolean; env?: string | undefined }): { preference: ModelPreference | null; source: DefaultModelSource } {
|
|
643
|
+
if (input.requested) return { preference: input.requested, source: 'request' }
|
|
644
|
+
if (input.sessionModel) return { preference: input.sessionModel, source: 'session' }
|
|
645
|
+
// model-router routes a session bound to a profile through that profile when the request names no model.
|
|
646
|
+
if (input.sessionProfile) return { preference: null, source: 'profile' }
|
|
647
|
+
const env = closedEnvDefaultModel(input.env)
|
|
648
|
+
if (env) return { preference: env, source: 'env' }
|
|
649
|
+
return { preference: DEFAULT_MODEL, source: 'built-in' }
|
|
650
|
+
}
|
|
651
|
+
// A session id is only looked up in the in-memory session map (an unknown id reads null), so nothing else is checked.
|
|
652
|
+
function defaultModelField(sessionQuery: unknown): { preference: ModelPreference | null; source: DefaultModelSource } {
|
|
653
|
+
const sid = typeof sessionQuery === 'string' && sessionQuery.length <= 128 ? sessionQuery : ''
|
|
654
|
+
const sessionModel = sid ? getSessionModel(sid) : null
|
|
655
|
+
const sessionProfile = !!sid && !sessionModel && isProfilePreference(getSessionBinding(sid))
|
|
656
|
+
return resolveDefaultModel({ sessionModel, sessionProfile, env: process.env.COS_G2_DEFAULT_MODEL })
|
|
657
|
+
}
|
|
658
|
+
|
|
628
659
|
// Stable app slots backed by Codex + Cursor live catalogs. Authenticated by
|
|
629
660
|
// global /api middleware; ?refresh=1 forces discovery.
|
|
630
661
|
healthRouter.get('/models', async (req, res) => {
|
|
@@ -656,6 +687,8 @@ healthRouter.get('/models', async (req, res) => {
|
|
|
656
687
|
res.json({
|
|
657
688
|
...catalog,
|
|
658
689
|
...threadAttachHealthFields(threadAttach),
|
|
690
|
+
// 7.0.2 (A7): what a picker's "Default" resolves to (?sessionId= adds that session's preference).
|
|
691
|
+
defaultModel: defaultModelField(req.query.sessionId),
|
|
659
692
|
// 6.62.0 (plans 3.5, 3.11): the phone reads THIS surface, so the live-Continue counters
|
|
660
693
|
// and the per-provider action contract are published here too, same helpers as health.
|
|
661
694
|
...continueLiveHealthFields(),
|
|
@@ -680,6 +713,8 @@ healthRouter.get('/models', async (req, res) => {
|
|
|
680
713
|
ollamaReady: isOllamaProviderReady(),
|
|
681
714
|
// Key lanes (plan v4 3.1): `keyProfiles` only while a profile is active. Absent when off.
|
|
682
715
|
...keyProfilesModelsField(),
|
|
716
|
+
// PH plan P1: where the calendar comes from. Counts only, never titles; not on public /api/health.
|
|
717
|
+
calendar: calendarStatus(),
|
|
683
718
|
serverInstanceId: getServerInstanceId(),
|
|
684
719
|
// Minted here rather than on a route of its own: this response is already
|
|
685
720
|
// authenticated, and the mint is stateless, so it costs nothing and stores
|