@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
|
@@ -1455,12 +1455,31 @@ export async function listAgentSessions(
|
|
|
1455
1455
|
return rows.slice(0, limit)
|
|
1456
1456
|
}
|
|
1457
1457
|
|
|
1458
|
+
/**
|
|
1459
|
+
* Every Claude transcript by its lowercase id (names only, no stat). `shouldStop` lets a bounded caller end the walk
|
|
1460
|
+
* between folders; a stopped walk answers what it has.
|
|
1461
|
+
*/
|
|
1462
|
+
export async function loadClaudeTranscriptIndex(roots: AgentSessionRoots, shouldStop: () => boolean = () => false): Promise<Map<string, string[]>> {
|
|
1463
|
+
const files = new Map<string, string[]>()
|
|
1464
|
+
for (const folder of await dirents(roots.claudeProjects)) {
|
|
1465
|
+
if (shouldStop()) break
|
|
1466
|
+
const dir = join(roots.claudeProjects, folder)
|
|
1467
|
+
for (const name of await dirents(dir)) {
|
|
1468
|
+
if (!CLAUDE_UUID_JSONL.test(name)) continue
|
|
1469
|
+
const id = name.slice(0, -6).toLowerCase()
|
|
1470
|
+
files.set(id, [...(files.get(id) || []), join(dir, name)])
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return files
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1458
1476
|
export async function findAgentSessionFile(
|
|
1459
1477
|
provider: AgentProvider,
|
|
1460
1478
|
sessionId: string,
|
|
1461
1479
|
roots: AgentSessionRoots,
|
|
1462
1480
|
now = new Date(),
|
|
1463
1481
|
preloadedAliases?: ClaudeAliasMap,
|
|
1482
|
+
preloaded?: { claudeFiles?: ReadonlyMap<string, string[]>; codexFiles?: readonly string[] },
|
|
1464
1483
|
): Promise<string | null> {
|
|
1465
1484
|
if (!isSafeSessionId(sessionId)) return null
|
|
1466
1485
|
const needle = sessionId.trim().toLowerCase()
|
|
@@ -1473,15 +1492,8 @@ export async function findAgentSessionFile(
|
|
|
1473
1492
|
// Names only. This used to lstat every transcript on this Mac (1,697 of
|
|
1474
1493
|
// them) to keep directories out of the candidate set, on every call. The
|
|
1475
1494
|
// isFile check now runs on the one id that matched, below.
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
const dir = join(roots.claudeProjects, folder)
|
|
1479
|
-
for (const name of await dirents(dir)) {
|
|
1480
|
-
if (!CLAUDE_UUID_JSONL.test(name)) continue
|
|
1481
|
-
const id = name.slice(0, -6).toLowerCase()
|
|
1482
|
-
files.set(id, [...(files.get(id) || []), join(dir, name)])
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1495
|
+
// 7.0.1: a caller resolving several ids at once (work-session-activity.ts) builds this index once and hands it in.
|
|
1496
|
+
const files = preloaded?.claudeFiles ?? await loadClaudeTranscriptIndex(roots)
|
|
1485
1497
|
const exact = aliases.has(needle) || files.has(needle)
|
|
1486
1498
|
const ids = new Set<string>()
|
|
1487
1499
|
for (const [id, head] of aliases) {
|
|
@@ -1498,8 +1510,9 @@ export async function findAgentSessionFile(
|
|
|
1498
1510
|
return matches.length === 1 ? matches[0] : null
|
|
1499
1511
|
}
|
|
1500
1512
|
if (provider === 'codex') {
|
|
1501
|
-
|
|
1502
|
-
|
|
1513
|
+
const lists = preloaded?.codexFiles ? [preloaded.codexFiles] : await Promise.all(codexSessionRootsOf(roots).map(root => listCodexJsonlFiles(root)))
|
|
1514
|
+
for (const list of lists) {
|
|
1515
|
+
for (const file of list) {
|
|
1503
1516
|
const name = file.split('/').pop()?.toLowerCase() ?? ''
|
|
1504
1517
|
if (name.includes(needle)) return file
|
|
1505
1518
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Images a session LOOKED AT: the pictures that came back inside tool results (a Read of a PNG,
|
|
2
|
+
// a browser or computer-use screenshot) and the pictures a person attached to a prompt.
|
|
3
|
+
//
|
|
4
|
+
// WHY. Miles, 11/08: the Claude app shows a thumbnail under a step like "Ran 2 commands, read 2
|
|
5
|
+
// files", labelled "Read" and "1 of 2". Control Web showed only words for that turn. 6.99.28
|
|
6
|
+
// covered images written into reply text and 6.99.35 covered SendUserFile; neither covered tool
|
|
7
|
+
// results or prompt attachments.
|
|
8
|
+
//
|
|
9
|
+
// RECORD SHAPES (read from real transcripts on this Mac, 11/08, shapes only):
|
|
10
|
+
// - Claude, a Read of an image: a `user` row with `toolUseResult: { type: 'image', file: { base64,
|
|
11
|
+
// type, originalSize, dimensions } }` and `message.content: [{ type: 'tool_result', tool_use_id,
|
|
12
|
+
// content: [{ type: 'image', source: { type: 'base64', media_type, data } }] }]`.
|
|
13
|
+
// - Claude, a screenshot (Claude_Browser / claude-in-chrome / computer-use / Claude_Preview): the
|
|
14
|
+
// same tool_result block, its `content` a mix of text blocks and image blocks of base64 source.
|
|
15
|
+
// - Claude, a prompt attachment: a plain `user` row (no toolUseResult) whose `message.content`
|
|
16
|
+
// holds `{ type: 'image', source: { type: 'base64', ... } }` beside the text block. A few rows
|
|
17
|
+
// (sent through the SDK) carry the image and no words at all.
|
|
18
|
+
// - Codex: `response_item` rows. A tool output is `function_call_output` or
|
|
19
|
+
// `custom_tool_call_output` with `output: [{ type: 'input_text' }, { type: 'input_image',
|
|
20
|
+
// image_url: 'data:image/png;base64,...', detail }]`; a prompt attachment is a user `message`
|
|
21
|
+
// whose `content` holds `input_image` blocks. The calls were `exec`, `wait` and `js`.
|
|
22
|
+
// - Every image seen was embedded base64 (png, jpeg, webp). None referenced a path only. The
|
|
23
|
+
// path-only Read is still handled, under the workspace rules output images use.
|
|
24
|
+
//
|
|
25
|
+
// This module is pure: it finds the images and names them. Ids, the workspace check and the
|
|
26
|
+
// bytes live in agent-session-output-images.ts, beside the other previews.
|
|
27
|
+
import type { AgentProvider } from './agent-session-store.js'
|
|
28
|
+
|
|
29
|
+
export type ToolImageLabel = 'Read' | 'Screenshot' | 'Image' | 'Attached'
|
|
30
|
+
export interface SessionToolImage {
|
|
31
|
+
/** Opaque, process-scoped: `ti_` and 64 hex characters. */
|
|
32
|
+
id: string
|
|
33
|
+
/** What the step was, as the Claude app labels it: "Read", "Screenshot", or "Attached" for a prompt image. */
|
|
34
|
+
tool: ToolImageLabel
|
|
35
|
+
/** Its place in the step, 1-based: "1 of 2". */
|
|
36
|
+
index: number
|
|
37
|
+
total: number
|
|
38
|
+
state: 'available' | 'unavailable'
|
|
39
|
+
/**
|
|
40
|
+
* `too_large`: the embedded image is over MAX_TOOL_IMAGE_BYTES. `outside_workspace` and
|
|
41
|
+
* `unavailable`: a path-only Read, under the output-image rules. `missing`: no bytes and no path.
|
|
42
|
+
* `secret_file` (7.0.4): a Read (path-only, or with its bytes embedded) of a file whose name says it may hold secrets
|
|
43
|
+
* (lib/secret-path-guard.ts). Embedded bytes with no path (a screenshot, a prompt attachment) have no name to judge.
|
|
44
|
+
*/
|
|
45
|
+
reason?: 'outside_workspace' | 'unavailable' | 'too_large' | 'missing' | 'secret_file'
|
|
46
|
+
/** 7.0.4: the words the card shows instead, set only with `secret_file`. */
|
|
47
|
+
notice?: string
|
|
48
|
+
/** The step came before the turn's words (a reply that opened with tool calls). */
|
|
49
|
+
before?: true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SESSION_TOOL_IMAGE_ID = /^ti_[a-f0-9]{64}$/
|
|
53
|
+
/** Thumbnails one turn may show. The rest are counted ("+N more") and never get an id. */
|
|
54
|
+
export const TOOL_IMAGES_PER_TURN = 6
|
|
55
|
+
/** The largest decoded image served. The biggest seen on this Mac was about 510 KB. */
|
|
56
|
+
export const MAX_TOOL_IMAGE_BYTES = 8 * 1024 * 1024
|
|
57
|
+
export const TOOL_IMAGE_TYPES: readonly string[] = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']
|
|
58
|
+
const IMAGE_PATH = /\.(png|jpe?g|webp|gif)$/i
|
|
59
|
+
|
|
60
|
+
export type ToolImageRef =
|
|
61
|
+
/** `path`: the file a Read named, when its bytes are embedded (7.0.4 QA: the secret guard judges that name). */
|
|
62
|
+
| { kind: 'embedded'; mediaType: string; data: string; path?: string }
|
|
63
|
+
| { kind: 'path'; path: string }
|
|
64
|
+
| { kind: 'missing' }
|
|
65
|
+
export interface ToolUseMeta { name: string; path?: string }
|
|
66
|
+
export interface FoundToolImage { tool: ToolImageLabel; ref: ToolImageRef }
|
|
67
|
+
|
|
68
|
+
/** The Claude app's label for a tool. Pure. */
|
|
69
|
+
export function toolLabel(name: string | undefined): ToolImageLabel {
|
|
70
|
+
if (name === 'Read') return 'Read'
|
|
71
|
+
if (name && /screenshot|computer|zoom|browser_batch|app_batch|snapshot/i.test(name)) return 'Screenshot'
|
|
72
|
+
return 'Image'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Decoded size of a base64 string, without decoding it. */
|
|
76
|
+
export function estimateBytes(b64: string): number {
|
|
77
|
+
let pad = 0
|
|
78
|
+
if (b64.endsWith('==')) pad = 2; else if (b64.endsWith('=')) pad = 1
|
|
79
|
+
return Math.max(0, Math.floor(b64.length * 3 / 4) - pad)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The image type the bytes ARE, from their magic numbers, or null. Never the declared type. */
|
|
83
|
+
export function sniffImageType(bytes: Buffer): string | null {
|
|
84
|
+
if (bytes.length >= 8 && bytes.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return 'image/png'
|
|
85
|
+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'image/jpeg'
|
|
86
|
+
if (bytes.length >= 6 && /^GIF8[79]a$/.test(bytes.subarray(0, 6).toString('latin1'))) return 'image/gif'
|
|
87
|
+
if (bytes.length >= 12 && bytes.subarray(0, 4).toString('latin1') === 'RIFF' && bytes.subarray(8, 12).toString('latin1') === 'WEBP') return 'image/webp'
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Decode an embedded image: refused over the cap (checked before decoding) or when the bytes are not a safe image type. */
|
|
92
|
+
export function decodeToolImage(ref: { mediaType: string; data: string }): { status: 'ok'; bytes: Buffer; type: string } | { status: 'too_large' | 'unavailable' } {
|
|
93
|
+
if (estimateBytes(ref.data) > MAX_TOOL_IMAGE_BYTES) return { status: 'too_large' }
|
|
94
|
+
const bytes = Buffer.from(ref.data, 'base64')
|
|
95
|
+
if (!bytes.length || bytes.length > MAX_TOOL_IMAGE_BYTES) return { status: bytes.length ? 'too_large' : 'unavailable' }
|
|
96
|
+
const type = sniffImageType(bytes)
|
|
97
|
+
return type ? { status: 'ok', bytes, type } : { status: 'unavailable' }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function obj(v: unknown): Record<string, unknown> | null { return v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : null }
|
|
101
|
+
|
|
102
|
+
/** An Anthropic image block of base64 source with a safe declared type, or null. */
|
|
103
|
+
function base64Block(block: unknown): ToolImageRef | null {
|
|
104
|
+
const b = obj(block); if (!b || b.type !== 'image') return null
|
|
105
|
+
const s = obj(b.source)
|
|
106
|
+
if (!s || s.type !== 'base64' || typeof s.data !== 'string' || !s.data || typeof s.media_type !== 'string') return null
|
|
107
|
+
if (!TOOL_IMAGE_TYPES.includes(s.media_type)) return null
|
|
108
|
+
return { kind: 'embedded', mediaType: s.media_type, data: s.data }
|
|
109
|
+
}
|
|
110
|
+
/** A Codex/OpenAI `input_image` block carrying a data URL of a safe type, or null. Remote URLs stay inert. */
|
|
111
|
+
function dataUrlBlock(block: unknown): ToolImageRef | null {
|
|
112
|
+
const b = obj(block); if (!b || b.type !== 'input_image' || typeof b.image_url !== 'string') return null
|
|
113
|
+
const m = /^data:(image\/(?:png|jpeg|webp|gif));base64,/.exec(b.image_url.slice(0, 64))
|
|
114
|
+
if (!m) return null
|
|
115
|
+
return { kind: 'embedded', mediaType: m[1], data: b.image_url.slice(m[0].length) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Keep the source of a Codex view_image so embedded bytes pass the same filename guard as Read. */
|
|
119
|
+
function codexImagePath(args: unknown): string | undefined {
|
|
120
|
+
if (typeof args === 'string') {
|
|
121
|
+
if (args.length > 16_384) return undefined
|
|
122
|
+
try { args = JSON.parse(args) } catch { return undefined }
|
|
123
|
+
}
|
|
124
|
+
const path = obj(args)?.path
|
|
125
|
+
return typeof path === 'string' && path.trim().length > 0 && path.length <= 4096 && !path.includes('\0') ? path : undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The tool calls a row makes, by call id: their names, and a Read's file path. Pure. */
|
|
129
|
+
export function toolUsesOf(provider: AgentProvider, row: Record<string, unknown>): Array<[string, ToolUseMeta]> {
|
|
130
|
+
if (row.isSidechain === true) return []
|
|
131
|
+
if (provider === 'claude') {
|
|
132
|
+
if (row.type !== 'assistant') return []
|
|
133
|
+
const content = obj(row.message)?.content
|
|
134
|
+
if (!Array.isArray(content)) return []
|
|
135
|
+
const out: Array<[string, ToolUseMeta]> = []
|
|
136
|
+
for (const block of content) {
|
|
137
|
+
const b = obj(block)
|
|
138
|
+
if (!b || b.type !== 'tool_use' || typeof b.id !== 'string' || typeof b.name !== 'string') continue
|
|
139
|
+
const input = obj(b.input)
|
|
140
|
+
const path = typeof input?.file_path === 'string' ? input.file_path : undefined
|
|
141
|
+
out.push([b.id, path ? { name: b.name, path } : { name: b.name }])
|
|
142
|
+
}
|
|
143
|
+
return out
|
|
144
|
+
}
|
|
145
|
+
if (provider === 'codex') {
|
|
146
|
+
if (row.type !== 'response_item') return []
|
|
147
|
+
const p = obj(row.payload)
|
|
148
|
+
if (!p || (p.type !== 'function_call' && p.type !== 'custom_tool_call') || typeof p.call_id !== 'string' || typeof p.name !== 'string') return []
|
|
149
|
+
const path = p.name === 'view_image' ? codexImagePath(p.type === 'function_call' ? p.arguments : p.input) : undefined
|
|
150
|
+
return [[p.call_id, path ? { name: p.name, path } : { name: p.name }]]
|
|
151
|
+
}
|
|
152
|
+
return []
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The images inside a row's tool results, in order, labelled by the tool that produced them.
|
|
157
|
+
* A sub-agent's results went to its parent, not to the person, and stay out. Pure.
|
|
158
|
+
*/
|
|
159
|
+
export function toolResultImages(provider: AgentProvider, row: Record<string, unknown>, uses: ReadonlyMap<string, ToolUseMeta>): FoundToolImage[] {
|
|
160
|
+
if (row.isSidechain === true) return []
|
|
161
|
+
const out: FoundToolImage[] = []
|
|
162
|
+
if (provider === 'claude') {
|
|
163
|
+
if (row.type !== 'user') return []
|
|
164
|
+
const content = obj(row.message)?.content
|
|
165
|
+
if (!Array.isArray(content)) return []
|
|
166
|
+
const results = content.filter(b => obj(b)?.type === 'tool_result')
|
|
167
|
+
for (const block of results) {
|
|
168
|
+
const b = obj(block)!
|
|
169
|
+
const meta = typeof b.tool_use_id === 'string' ? uses.get(b.tool_use_id) : undefined
|
|
170
|
+
const tool = toolLabel(meta?.name)
|
|
171
|
+
// 7.0.4 QA: a Read's embedded bytes keep the path it read, so `~/Desktop/credentials.png` is refused by name.
|
|
172
|
+
const readPath = meta?.name === 'Read' && meta.path ? meta.path : undefined
|
|
173
|
+
const found = Array.isArray(b.content) ? b.content.map(base64Block).filter((x): x is ToolImageRef => x !== null) : []
|
|
174
|
+
if (found.length) { for (const ref of found) out.push({ tool, ref: readPath && ref.kind === 'embedded' ? { ...ref, path: readPath } : ref }); continue }
|
|
175
|
+
// A Read of an image whose message block was not written: the row's own record of the
|
|
176
|
+
// result may still carry the bytes; else only the path the Read named; else nothing to show.
|
|
177
|
+
const result = obj(row.toolUseResult)
|
|
178
|
+
if (results.length !== 1 || b.is_error === true || result?.type !== 'image') continue
|
|
179
|
+
const file = obj(result.file)
|
|
180
|
+
if (file && typeof file.base64 === 'string' && file.base64 && typeof file.type === 'string' && TOOL_IMAGE_TYPES.includes(file.type)) {
|
|
181
|
+
out.push({ tool, ref: { kind: 'embedded', mediaType: file.type, data: file.base64, ...(readPath ? { path: readPath } : {}) } }); continue
|
|
182
|
+
}
|
|
183
|
+
if (meta?.name === 'Read' && meta.path && IMAGE_PATH.test(meta.path)) out.push({ tool, ref: { kind: 'path', path: meta.path } })
|
|
184
|
+
else out.push({ tool, ref: { kind: 'missing' } })
|
|
185
|
+
}
|
|
186
|
+
return out
|
|
187
|
+
}
|
|
188
|
+
if (provider === 'codex') {
|
|
189
|
+
if (row.type !== 'response_item') return []
|
|
190
|
+
const p = obj(row.payload)
|
|
191
|
+
if (!p || (p.type !== 'function_call_output' && p.type !== 'custom_tool_call_output') || !Array.isArray(p.output)) return []
|
|
192
|
+
const meta = typeof p.call_id === 'string' ? uses.get(p.call_id) : undefined
|
|
193
|
+
const tool = meta?.name === 'view_image' ? 'Read' : toolLabel(meta?.name)
|
|
194
|
+
for (const block of p.output) {
|
|
195
|
+
const ref = dataUrlBlock(block)
|
|
196
|
+
if (!ref || ref.kind !== 'embedded') continue
|
|
197
|
+
// A known file-read without a trustworthy source cannot bypass secret filename checks.
|
|
198
|
+
// Other image tools (for example exec screenshots) do not name a source file.
|
|
199
|
+
out.push({ tool, ref: meta?.name === 'view_image' ? meta.path ? { ...ref, path: meta.path } : { kind: 'missing' } : ref })
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The images a person attached to a prompt row, or [] (a tool result, a sub-agent, an injected
|
|
207
|
+
* row, an assistant row). Whether the row is a turn at all is the turn reader's call. Pure.
|
|
208
|
+
*/
|
|
209
|
+
export function promptImages(provider: AgentProvider, row: Record<string, unknown>): ToolImageRef[] {
|
|
210
|
+
if (row.isSidechain === true || row.isMeta === true || row.isCompactSummary === true) return []
|
|
211
|
+
if (provider === 'claude') {
|
|
212
|
+
if (row.type !== 'user' || row.toolUseResult) return []
|
|
213
|
+
const content = obj(row.message)?.content
|
|
214
|
+
if (!Array.isArray(content) || content.some(b => obj(b)?.type === 'tool_result')) return []
|
|
215
|
+
return content.map(base64Block).filter((x): x is ToolImageRef => x !== null)
|
|
216
|
+
}
|
|
217
|
+
if (provider === 'codex') {
|
|
218
|
+
if (row.type !== 'response_item') return []
|
|
219
|
+
const p = obj(row.payload)
|
|
220
|
+
if (!p || p.type !== 'message' || p.role !== 'user' || !Array.isArray(p.content)) return []
|
|
221
|
+
return p.content.map(dataUrlBlock).filter((x): x is ToolImageRef => x !== null)
|
|
222
|
+
}
|
|
223
|
+
return []
|
|
224
|
+
}
|
|
@@ -33,7 +33,9 @@ import {
|
|
|
33
33
|
payloadText,
|
|
34
34
|
type AgentProvider,
|
|
35
35
|
} from './agent-session-store.js'
|
|
36
|
-
import type { SessionOutputImage } from './agent-session-output-images.js'
|
|
36
|
+
import type { SessionOutputFile, SessionOutputImage } from './agent-session-output-images.js'
|
|
37
|
+
import type { SessionToolImage } from './agent-session-tool-images.js'
|
|
38
|
+
import type { SessionOutputLink } from './session-links.js'
|
|
37
39
|
import { unwrapPeerMessage } from './session-stream-events.js'
|
|
38
40
|
import { withoutTurnFooter } from './turn-footer.js'
|
|
39
41
|
|
|
@@ -44,9 +46,23 @@ export interface SessionTurn {
|
|
|
44
46
|
at?: string
|
|
45
47
|
/** Opt-in native-session output previews. No local paths or COS message identities. */
|
|
46
48
|
outputImages?: SessionOutputImage[]
|
|
49
|
+
/** Files a Claude session sent with SendUserFile that are not normalized images (6.99.35). Names only, no paths. */
|
|
50
|
+
outputFiles?: SessionOutputFile[]
|
|
51
|
+
/** Preview engine phase 1: local Markdown links in this reply, as cards. The text carries `[words](cos-file:N)`; base names only, no paths. */
|
|
52
|
+
outputLinks?: SessionOutputLink[]
|
|
53
|
+
/** Images the session looked at in the step around this turn (tool results), or a prompt's attached images. Opaque ids, no bytes, no paths. */
|
|
54
|
+
toolImages?: SessionToolImage[]
|
|
55
|
+
/** Images in that step past TOOL_IMAGES_PER_TURN: counted, never given an id. */
|
|
56
|
+
toolImagesMore?: number
|
|
57
|
+
/** 6.99.35: this message had images or files and "Show files and images your sessions make" is off. No names travel. */
|
|
58
|
+
previewsOff?: true
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
export type SessionTurnProjection =
|
|
61
|
+
export type SessionTurnProjection = {
|
|
62
|
+
(provider: AgentProvider, record: Record<string, unknown>, turn: SessionTurn | null): SessionTurn | null
|
|
63
|
+
/** A growing read reparses its window from scratch; discard state from the previous pass. */
|
|
64
|
+
reset?: () => void
|
|
65
|
+
}
|
|
50
66
|
|
|
51
67
|
/** What a client may ask for in one read. Forty messages is ~10 lens pages of words. */
|
|
52
68
|
export const SESSION_TURNS_MAX = 40
|
|
@@ -137,6 +153,7 @@ export function sessionTurnFromRecord(provider: AgentProvider, obj: Record<strin
|
|
|
137
153
|
|
|
138
154
|
/** The newest `limit` messages among these lines (file order in, file order out). */
|
|
139
155
|
export function recentSessionTurns(provider: AgentProvider, lines: readonly string[], limit: number, project?: SessionTurnProjection): SessionTurn[] {
|
|
156
|
+
project?.reset?.()
|
|
140
157
|
const out: SessionTurn[] = []
|
|
141
158
|
for (const line of lines) {
|
|
142
159
|
const obj = parseJsonLine(line)
|
package/server/lib/api-auth.ts
CHANGED
|
@@ -44,6 +44,9 @@ const DISPLAY_STREAM_CAPABILITY_PATH = /^\/display-stream\/\d{1,15}\.[0-9a-f]{64
|
|
|
44
44
|
// Minting, status and decisions stay behind the token AND a loopback socket.
|
|
45
45
|
const PAIRING_POLL_PATH = new RegExp(`^/pairing/claim/${PAIRING_NONCE_BODY}$`)
|
|
46
46
|
|
|
47
|
+
/** `res.locals` key the gate sets to `'token'` when the X-COS-Token header matched. */
|
|
48
|
+
export const API_AUTH_BY_LOCAL = 'cosApiAuthBy'
|
|
49
|
+
|
|
47
50
|
export function isPublicApiRequest(method: string, path: string): boolean {
|
|
48
51
|
if (PUBLIC_API_PATHS.has(path)) return true
|
|
49
52
|
if (method === 'POST' && path === '/pairing/claim') return true
|
|
@@ -76,6 +79,9 @@ export function requireApiToken(apiToken: string, options: RequireApiTokenOption
|
|
|
76
79
|
return (req, res, next) => {
|
|
77
80
|
if (isPublicApiRequest(req.method, req.path)) return next()
|
|
78
81
|
if (timingSafeTokenEqual(req.headers['x-cos-token'], apiToken)) {
|
|
82
|
+
// 7.0.4 W1: which door let this request in, for routes that answer the Mac's own helper differently from a
|
|
83
|
+
// browser (lib/native-mac-caller.ts). Set only here, after the header matched; a cookie session never sets it.
|
|
84
|
+
res.locals[API_AUTH_BY_LOCAL] = 'token'
|
|
79
85
|
if (options.onAuthenticated) {
|
|
80
86
|
try { options.onAuthenticated(req) } catch { /* observer only */ }
|
|
81
87
|
}
|
|
@@ -82,7 +82,9 @@ export function prepareAskWorkPlan(request: AskWorkPlanRequest, task: TaskBoardR
|
|
|
82
82
|
const evidenceStatus = { snapshotAgeSec: evidence.snapshotAgeSec, liveListAgeSec: evidence.liveListAgeSec,
|
|
83
83
|
logScanComplete: evidence.logScanComplete, fresh, usageMode: 'cached',
|
|
84
84
|
scope: 'Canonical task and server session evidence only; no local attachments, local link context, or chat history. Meeting references do not include transcripts.',
|
|
85
|
-
|
|
85
|
+
// A row is wholly disabled only when neither Continue nor Fork is eligible. The
|
|
86
|
+
// individual automatic/forkable fields still constrain each assignment mode.
|
|
87
|
+
disabledTargets: evidence.rows.filter((_, index) => !sessions[index]!.automatic && !sessions[index]!.forkable).map(row => ({ id: row.id,
|
|
86
88
|
reasons: [!fresh && 'stale_or_missing_evidence', !evidence.logScanComplete && 'incomplete_session_scan',
|
|
87
89
|
!row.member && 'not_card_member', !['claude', 'codex'].includes(row.id.split(':')[0]!) && 'unsupported_provider',
|
|
88
90
|
!providerAllowed(row.id.split(':')[0]!) && 'provider_usage_unavailable', row.busy && 'busy', !row.live && 'not_live',
|