Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
a69eb36
docs: add fresh agent platform implementation plan
Apr 18, 2026
221955e
docs: revise fresh agent implementation plan
Apr 18, 2026
f0f217e
docs: tighten fresh agent implementation plan
Apr 18, 2026
5f43865
docs: tighten fresh agent platform plan
Apr 18, 2026
1eec877
docs: tighten fresh agent implementation plan
Apr 18, 2026
922e4bb
docs: tighten fresh agent platform plan
Apr 18, 2026
c658be6
docs: tighten fresh agent plan review fixes
Apr 18, 2026
9c92253
docs: tighten fresh agent plan review findings
Apr 18, 2026
311566d
docs: resolve final fresheyes nits
Apr 18, 2026
a952b7b
docs: add fresh agent platform test plan
Apr 18, 2026
8c0b61f
refactor: add fresh-agent persistence vocabulary
Apr 18, 2026
580885f
feat: add fresh agent transport and read models
Apr 18, 2026
3f83456
refactor: move claude runtime behind fresh agent adapter
Apr 18, 2026
2971c7c
feat: add codex rich runtime support
Apr 18, 2026
d28378c
test: add codex rich thread fixture
Apr 18, 2026
37f5e99
Implement fresh-agent Codex thread surface
Apr 18, 2026
4644e0c
Fix fresh-agent resume history tests
Apr 18, 2026
0aca051
Implement fresh-agent transport and coverage
Apr 18, 2026
cef7c68
Complete fresh-agent shared shell cutover
Apr 18, 2026
5c7ce3c
Fix fresh-agent recovery regressions
Apr 18, 2026
015abd8
Fix fresh-agent error and question banners
Apr 18, 2026
43a215b
Fix fresh-agent resume and live reload flow
Apr 18, 2026
9889600
Fix freshcodex registry and codex capabilities
Apr 18, 2026
e3767e9
Fix fresh-agent lost-session transport recovery
Apr 18, 2026
6af7bee
Surface Codex shared-shell metadata
Apr 19, 2026
6b617a9
docs: plan freshcodex contract foundation
May 1, 2026
0bea85f
docs: tighten freshcodex contract plan
May 3, 2026
a4a20c5
docs: fix freshcodex protocol plan
May 3, 2026
ae18e8b
docs: tighten freshcodex runtime plan
May 3, 2026
2a59dd5
docs: require live freshcodex notifications
May 3, 2026
1ea93c0
Tighten Freshcodex contract plan
May 3, 2026
1f7e458
Ground freshcodex plan in local Codex schema
May 3, 2026
63c0a18
Tighten freshcodex protocol plan
May 3, 2026
3fc0b82
Tighten freshcodex planning closure
May 4, 2026
b6467ed
Tighten freshcodex foundation plan
May 4, 2026
6efa07d
Tighten Freshcodex settings foundation plan
May 4, 2026
d365b95
Tighten freshcodex plan task boundaries
May 4, 2026
5e2cb93
Tighten freshcodex schema contract plan
May 4, 2026
013651b
Tighten Freshcodex schema coverage plan
May 4, 2026
a6144b9
Preserve Freshcodex thread list pagination
May 4, 2026
05fb156
Tighten Freshcodex schema history plan
May 4, 2026
76fa27d
Tighten Freshcodex schema leaf coverage
May 4, 2026
3633b8b
Tighten freshcodex request routing plan
May 4, 2026
b43abfa
Fix plan: type name, backwardsCursor, tokenUsage tracking
May 4, 2026
b513c79
Add Freshcodex contract foundation test plan
May 4, 2026
72b9993
Add comprehensive E2E browser test coverage with screenshots, orchest…
May 4, 2026
c525da6
Tighten freshcodex plan sync and dynamic tool coverage
May 4, 2026
3a9bf4c
Tighten Freshcodex locator and schema plan
May 4, 2026
90ed63f
Tighten Freshcodex schema audit and model pagination
May 4, 2026
2662771
Tighten Freshcodex locator state and sandbox schema
May 4, 2026
06c17ff
Tighten Freshcodex request id and turn schemas
May 4, 2026
c29e8d7
Tighten Freshcodex model capabilities and request ids
May 4, 2026
4c710b9
Align Freshcodex plan with JSON wire requiredness
May 4, 2026
32ff278
Route legacy Codex server approvals
May 4, 2026
57f1249
Align legacy approval wire requiredness
May 4, 2026
1be461b
Keep Freshcodex resume page-first
May 4, 2026
475c809
Tighten Freshcodex runtime setting contracts
May 4, 2026
d386315
Tighten Freshcodex item normalization plan
May 4, 2026
2dad7c2
Preserve Freshcodex generated item details
May 4, 2026
1a3c6ea
Preserve Codex tool argument JSON
May 4, 2026
a076d85
Route Codex notifications by generated locators
May 4, 2026
31e33be
Make Codex outbound params strict
May 4, 2026
4726451
Specify fresh-agent referenced schemas
May 4, 2026
9181cd7
Synthesize Freshcodex schema traceability plan
May 4, 2026
5032789
Implement Freshcodex contract traceability foundation
May 8, 2026
e17700a
Make fresh-agent runtime use canonical locators
May 8, 2026
961de0f
Harden Freshcodex actions and locator tests
May 8, 2026
213b9d7
Harden fresh-agent restore and reconnect lifecycle
May 8, 2026
d11cb37
Update Freshcodex test plan cost model
May 8, 2026
d3d986f
Fix freshcodex dev rebase integration
May 8, 2026
8cc8512
Fix Freshcodex review regressions
May 9, 2026
e80810b
Harden fresh-agent create lifecycle
May 9, 2026
5cc3e59
Plan Freshcodex full-suite stabilization
May 9, 2026
d0eb42f
Synthesize freshcodex suite stabilization plan
May 9, 2026
03d06e4
Synthesize freshcodex stabilization plan
May 9, 2026
62a7980
Synthesize freshcodex stabilization plan
May 9, 2026
2e71823
Synthesize freshcodex stabilization plan
May 9, 2026
884459d
Synthesize freshcodex stabilization boundaries
May 9, 2026
59a305a
Add freshcodex boundary closure matrix
May 9, 2026
ca40fa4
Canonicalize fresh-agent pane identity
May 9, 2026
976d3d4
Repair fresh-agent persistence migrations
May 9, 2026
0315d0e
Normalize fresh-agent settings compatibility
May 9, 2026
ed61654
Harden fresh-agent recovery identity
May 9, 2026
c3914e8
Stabilize legacy agent-chat harness
May 9, 2026
224c884
Close freshcodex full-suite regressions
May 9, 2026
0e67ac4
Close freshcodex post-implementation review issues (R1-R19)
May 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions docs/plans/2026-04-18-fresh-agent-platform-test-plan.md

Large diffs are not rendered by default.

915 changes: 915 additions & 0 deletions docs/plans/2026-04-18-fresh-agent-platform.md

Large diffs are not rendered by default.

5,564 changes: 5,564 additions & 0 deletions docs/plans/2026-04-30-freshcodex-contract-foundation.md

Large diffs are not rendered by default.

474 changes: 474 additions & 0 deletions docs/plans/2026-05-03-freshcodex-contract-foundation-test-plan.md

Large diffs are not rendered by default.

1,979 changes: 1,979 additions & 0 deletions docs/plans/2026-05-09-freshcodex-full-suite-stabilization.md

Large diffs are not rendered by default.

72 changes: 72 additions & 0 deletions scripts/audit-codex-app-server-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { execFileSync } from 'node:child_process'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'

import {
CODEX_CLIENT_REQUEST_METHODS,
CODEX_RUNTIME_LEAF_VALUES,
CODEX_SCHEMA_VERSION,
CODEX_SERVER_NOTIFICATION_METHODS,
CODEX_SERVER_REQUEST_METHODS,
CODEX_THREAD_ITEM_VARIANTS,
} from '../test/fixtures/coding-cli/codex-app-server/schema-inventory.js'

type JsonSchema = {
oneOf?: Array<{ properties?: { method?: { enum?: string[] }; type?: { const?: string; enum?: string[] } } }>
definitions?: Record<string, JsonSchema & { enum?: string[] }>
enum?: string[]
}

function readSchema(filePath: string): JsonSchema {
return JSON.parse(readFileSync(filePath, 'utf8')) as JsonSchema
}

function methods(filePath: string): string[] {
return (readSchema(filePath).oneOf ?? [])
.map((entry) => entry.properties?.method?.enum?.[0])
.filter((value): value is string => Boolean(value))
}

function threadItemVariants(filePath: string): string[] {
const schema = readSchema(filePath).definitions?.ThreadItem
return (schema?.oneOf ?? [])
.map((entry) => entry.properties?.type?.const ?? entry.properties?.type?.enum?.[0])
.filter((value): value is string => Boolean(value))
}

function compare(label: string, expected: readonly string[], actual: readonly string[]): string[] {
const missing = expected.filter((value) => !actual.includes(value))
const added = actual.filter((value) => !expected.includes(value))
const messages: string[] = []
if (missing.length > 0) messages.push(`${label} missing from generated schema: ${missing.join(', ')}`)
if (added.length > 0) messages.push(`${label} added by generated schema: ${added.join(', ')}`)
return messages
}

const workDir = mkdtempSync(path.join(tmpdir(), 'freshell-codex-schema-audit-'))
try {
const jsonDir = path.join(workDir, 'json')
execFileSync('codex', ['app-server', 'generate-json-schema', '--out', jsonDir], { stdio: 'inherit' })

const failures = [
...compare('client request methods', CODEX_CLIENT_REQUEST_METHODS, methods(path.join(jsonDir, 'ClientRequest.json'))),
...compare('server request methods', CODEX_SERVER_REQUEST_METHODS, methods(path.join(jsonDir, 'ServerRequest.json'))),
...compare('server notification methods', CODEX_SERVER_NOTIFICATION_METHODS, methods(path.join(jsonDir, 'ServerNotification.json'))),
...compare('thread item variants', CODEX_THREAD_ITEM_VARIANTS, threadItemVariants(path.join(jsonDir, 'codex_app_server_protocol.v2.schemas.json'))),
]

const v2Schema = readSchema(path.join(jsonDir, 'codex_app_server_protocol.v2.schemas.json'))
const generatedReasoningEffort = v2Schema.definitions?.ReasoningEffort?.enum ?? []
failures.push(...compare('reasoning effort values', CODEX_RUNTIME_LEAF_VALUES.reasoningEffort, generatedReasoningEffort))

if (failures.length > 0) {
console.error(`Codex app-server schema inventory is stale. Checked-in inventory version: ${CODEX_SCHEMA_VERSION}`)
for (const failure of failures) console.error(`- ${failure}`)
process.exit(1)
}

console.log(`Codex app-server schema inventory matches checked-in ${CODEX_SCHEMA_VERSION} fixture.`)
} finally {
rmSync(workDir, { recursive: true, force: true })
}
82 changes: 81 additions & 1 deletion server/agent-api/layout-schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,88 @@
import { z } from 'zod'
import { SessionLocatorSchema } from '../../shared/ws-protocol.js'

const FreshAgentContentSchema = z.object({
kind: z.literal('fresh-agent'),
sessionType: z.string().min(1),
provider: z.string().min(1),
createRequestId: z.string().min(1),
status: z.string().min(1),
sessionId: z.string().optional(),
resumeSessionId: z.string().optional(),
sessionRef: z.object({ provider: z.string().min(1), sessionId: z.string().min(1) }).optional(),
restoreError: z.object({ code: z.string().min(1), reason: z.string().min(1) }).optional(),
initialCwd: z.string().optional(),
model: z.string().optional(),
modelSelection: z.object({ kind: z.string().min(1), modelId: z.string().min(1) }).optional().or(z.null()),
permissionMode: z.string().optional(),
sandbox: z.enum(['read-only', 'workspace-write', 'danger-full-access']).optional(),
effort: z.string().optional(),
plugins: z.array(z.string()).optional(),
settingsDismissed: z.boolean().optional(),
}).passthrough().refine(
(v) => !(v.sessionRef && v.restoreError),
{ message: 'sessionRef and restoreError are mutually exclusive' },
)

const PaneNodeSchema: z.ZodType<any> = z.lazy(() => z.union([
z.object({ type: z.literal('leaf'), id: z.string(), content: z.record(z.string(), z.any()) }),
z.object({
type: z.literal('leaf'),
id: z.string(),
content: z.union([
z.object({
kind: z.literal('terminal'),
createRequestId: z.string(),
status: z.string(),
mode: z.string(),
terminalId: z.string().optional(),
shell: z.string().optional(),
resumeSessionId: z.string().optional(),
sessionRef: SessionLocatorSchema.optional(),
restoreError: z.object({ code: z.string(), reason: z.string() }).optional(),
initialCwd: z.string().optional(),
}).passthrough(),
z.object({
kind: z.literal('browser'),
browserInstanceId: z.string(),
url: z.string(),
devToolsOpen: z.boolean(),
}).passthrough(),
z.object({
kind: z.literal('editor'),
filePath: z.string().nullable(),
language: z.string().nullable(),
readOnly: z.boolean(),
content: z.string(),
viewMode: z.enum(['source', 'preview']),
}).passthrough(),
z.object({
kind: z.literal('picker'),
}).passthrough(),
FreshAgentContentSchema,
z.object({
kind: z.literal('agent-chat'),
provider: z.string(),
createRequestId: z.string(),
status: z.string(),
sessionId: z.string().optional(),
resumeSessionId: z.string().optional(),
sessionRef: SessionLocatorSchema.optional(),
restoreError: z.object({ code: z.string(), reason: z.string() }).optional(),
initialCwd: z.string().optional(),
modelSelection: z.object({ kind: z.string(), modelId: z.string() }).optional().or(z.null()),
permissionMode: z.string().optional(),
effort: z.string().optional(),
plugins: z.array(z.string()).optional(),
settingsDismissed: z.boolean().optional(),
}).passthrough(),
z.object({
kind: z.literal('extension'),
extensionName: z.string(),
props: z.record(z.string(), z.any()),
}).passthrough(),
z.object({ kind: z.string() }).passthrough(),
]),
}),
z.object({
type: z.literal('split'),
id: z.string(),
Expand Down
19 changes: 19 additions & 0 deletions server/agent-api/layout-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ export class LayoutStore {
}
}

if (content.kind === 'fresh-agent') {
switch (content.sessionType) {
case 'freshclaude':
return 'Freshclaude'
case 'freshcodex':
return 'Freshcodex'
case 'kilroy':
return 'Kilroy'
default:
return 'Fresh Agent'
}
}

if (content.kind === 'extension') {
return typeof content.extensionName === 'string' && content.extensionName
? content.extensionName
Expand Down Expand Up @@ -145,6 +158,12 @@ export class LayoutStore {
updateFromUi(snapshot: UiSnapshot, connectionId: string) {
this.snapshot = snapshot
this.sourceConnectionId = connectionId
for (const tab of snapshot.tabs) {
const leaves = this.collectLeaves(snapshot.layouts?.[tab.id], [])
for (const leaf of leaves) {
this.seedPaneTitle(tab.id, leaf.id, leaf.content)
}
}
}

getSourceConnectionId() {
Expand Down
32 changes: 32 additions & 0 deletions server/agent-timeline/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@ type TimelineCursorPayload = {

type TimelineMessageRecord = CanonicalTurn & { sessionId: string }

export type AgentTimelineSnapshot = {
sessionId: string
latestTurnId: string | null
revision: number
turns: CanonicalTurn[]
}

export type AgentTimelineService = {
getSnapshot: (query: { sessionId: string; revision?: number; signal?: AbortSignal }) => Promise<AgentTimelineSnapshot>
getTimelinePage: (query: AgentTimelinePageQuery & { sessionId: string; signal?: AbortSignal }) => Promise<AgentTimelinePage>
getTurnBody: (query: AgentTimelineTurnBodyQuery & { sessionId: string; turnId: string; signal?: AbortSignal }) => Promise<AgentTimelineTurn | null>
}
Expand Down Expand Up @@ -134,6 +142,30 @@ export function createAgentTimelineService(deps: AgentTimelineServiceDeps): Agen
}

return {
async getSnapshot({ sessionId, revision, signal }) {
throwIfAborted(signal)
const timeline = await loadTimeline(sessionId)
throwIfAborted(signal)
if (revision != null && revision !== timeline.revision) {
throw new RestoreStaleRevisionError(revision, timeline.revision)
}
return {
sessionId: timeline.sessionId,
latestTurnId: timeline.latestTurnId,
revision: timeline.revision,
turns: timeline.records
.slice()
.reverse()
.map((record) => ({
turnId: record.turnId,
messageId: record.messageId,
ordinal: record.ordinal,
source: record.source,
message: record.message,
})),
}
},

async getTimelinePage(query) {
throwIfAborted(query.signal)
if (query.revision == null) {
Expand Down
4 changes: 2 additions & 2 deletions server/claude-session-id.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
import { isCanonicalClaudeSessionId } from '../shared/session-contract.js'

export function isValidClaudeSessionId(value?: string): value is string {
return typeof value === 'string' && UUID_REGEX.test(value)
return isCanonicalClaudeSessionId(value)
}
Loading
Loading