Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
eac7a20
Add repo source foundation and session capabilities
cursoragent Apr 16, 2026
0a4be2f
Fix repo source foundation regressions
cursoragent Apr 16, 2026
2324b0b
Fix repo session runtime and test regressions
cursoragent Apr 16, 2026
e196933
Expand repo session editing surface
cursoragent Apr 16, 2026
6c51427
Add repo checks lifecycle and start job cutover
cursoragent Apr 16, 2026
fe0612a
Lazy load worker bundler in repo checks
cursoragent Apr 16, 2026
82c3691
Add job search projections
cursoragent Apr 16, 2026
89aa1ff
Start repo-backed skill execution
cursoragent Apr 16, 2026
2e02b20
Track source ids for saved app writes
cursoragent Apr 16, 2026
4d59ddc
Stabilize source-aware app save tests
cursoragent Apr 16, 2026
c190672
Relax app save UUID expectations
cursoragent Apr 16, 2026
ce949d4
Sync saved sources into repo artifacts
cursoragent Apr 16, 2026
92bb614
Prefer repo-backed source for saved apps
cursoragent Apr 16, 2026
027f906
Add job reindex maintenance hook
cursoragent Apr 16, 2026
1808a6a
Record published commits for synced sources
cursoragent Apr 16, 2026
cc43708
Fix repo-backed job execution
cursoragent Apr 16, 2026
ebba2ba
Fix repo session and job embed handling
cursoragent Apr 16, 2026
fb19e40
Fix job cleanup and repo capability registration
cursoragent Apr 16, 2026
806166e
Lazy load job execution runtime
cursoragent Apr 16, 2026
a88ceaa
Fix repo-backed review regressions
cursoragent Apr 16, 2026
0596d81
Tighten repo-backed review follow-ups
cursoragent Apr 17, 2026
13326c9
Honor forced repo session publish
cursoragent Apr 17, 2026
4f1a8c0
Clean up repo runtime sessions
cursoragent Apr 17, 2026
cd7f099
Tighten saved app and job search flows
cursoragent Apr 17, 2026
e1e1246
Refine repo publish and search reporting
cursoragent Apr 17, 2026
a71e5d8
Rollback saved app source state on failure
cursoragent Apr 17, 2026
26250cb
Use unique repo runtime session ids
cursoragent Apr 17, 2026
d6ce000
Normalize repo app params and merge imports
cursoragent Apr 17, 2026
69331f2
Harden repo search and align patch schemas
kentcdodds Apr 17, 2026
1915391
Fix ui save rollback commit capture
cursoragent Apr 17, 2026
571464b
Fix cache and caller context handling
cursoragent Apr 17, 2026
6fd87ea
Harden repo-backed session ownership
cursoragent Apr 17, 2026
fa0289c
Fix repo session search and origin credentials
cursoragent Apr 17, 2026
f054138
Delete vectors for due once-jobs
cursoragent Apr 17, 2026
74d9760
Refactor repo session helper boundaries
kentcdodds Apr 17, 2026
688097d
Extract repo session tree helpers
kentcdodds Apr 17, 2026
ad414e8
Fix repo session isolation and job storage fallback
cursoragent Apr 17, 2026
7d96125
Address remaining repo-backed review findings
cursoragent Apr 17, 2026
ee15fe3
Tighten remaining review edge cases
cursoragent Apr 17, 2026
1f4da7c
Fix ui save app sync rollback
cursoragent Apr 17, 2026
e544738
Fix job reindex secrets and repo session reuse
cursoragent Apr 17, 2026
ddb50b0
Polish remaining review fixes
cursoragent Apr 17, 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
307 changes: 193 additions & 114 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions packages/shared/src/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ export const mcpStorageContextSchema = object({
storageId: optional(nullable(string())),
})

export const mcpRepoContextSchema = object({
sourceId: optional(nullable(string())),
repoId: optional(nullable(string())),
sessionId: optional(nullable(string())),
sessionRepoId: optional(nullable(string())),
baseCommit: optional(nullable(string())),
manifestPath: optional(nullable(string())),
sourceRoot: optional(nullable(string())),
publishedCommit: optional(nullable(string())),
entityKind: optional(nullable(string())),
entityId: optional(nullable(string())),
})

const remoteConnectorRefSchema = object({
kind: remoteConnectorKindFieldSchema,
instanceId: remoteConnectorInstanceIdFieldSchema,
Expand All @@ -58,10 +71,12 @@ export const mcpCallerContextSchema = object({
homeConnectorId: optional(nullable(string())),
remoteConnectors: optional(nullable(array(remoteConnectorRefSchema))),
storageContext: optional(nullable(mcpStorageContextSchema)),
repoContext: optional(nullable(mcpRepoContextSchema)),
})

export type McpUserContext = InferOutput<typeof mcpUserContextSchema>
export type McpStorageContext = InferOutput<typeof mcpStorageContextSchema>
export type McpRepoContext = InferOutput<typeof mcpRepoContextSchema>
export type McpCallerContext = InferOutput<typeof mcpCallerContextSchema>

export const chatAgentPropsSchema = object({
Expand Down
22 changes: 22 additions & 0 deletions packages/worker/migrations/0023-entity-sources.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS entity_sources (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
entity_kind TEXT NOT NULL,
entity_id TEXT NOT NULL,
repo_id TEXT NOT NULL,
published_commit TEXT,
indexed_commit TEXT,
manifest_path TEXT NOT NULL DEFAULT 'kody.json',
source_root TEXT NOT NULL DEFAULT '/',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_entity_sources_user_entity
ON entity_sources(user_id, entity_kind, entity_id);

CREATE UNIQUE INDEX IF NOT EXISTS idx_entity_sources_repo_id
ON entity_sources(repo_id);

CREATE INDEX IF NOT EXISTS idx_entity_sources_user_id
ON entity_sources(user_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
CREATE TABLE IF NOT EXISTS repo_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
source_id TEXT NOT NULL,
session_repo_id TEXT NOT NULL,
session_repo_name TEXT NOT NULL,
session_repo_namespace TEXT NOT NULL DEFAULT 'default',
base_commit TEXT NOT NULL,
source_root TEXT NOT NULL DEFAULT '/',
conversation_id TEXT,
status TEXT NOT NULL DEFAULT 'active',
expires_at TEXT,
last_checkpoint_at TEXT,
last_checkpoint_commit TEXT,
last_check_run_id TEXT,
last_check_tree_hash TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);

CREATE INDEX IF NOT EXISTS idx_repo_sessions_user_id
ON repo_sessions(user_id);

CREATE INDEX IF NOT EXISTS idx_repo_sessions_source_id
ON repo_sessions(source_id);

CREATE INDEX IF NOT EXISTS idx_repo_sessions_conversation_id
ON repo_sessions(conversation_id);

ALTER TABLE mcp_skills
ADD COLUMN source_id TEXT;

CREATE INDEX IF NOT EXISTS idx_mcp_skills_source_id
ON mcp_skills(source_id);

ALTER TABLE ui_artifacts
ADD COLUMN source_id TEXT;

CREATE INDEX IF NOT EXISTS idx_ui_artifacts_source_id
ON ui_artifacts(source_id);

ALTER TABLE jobs
ADD COLUMN source_id TEXT;

ALTER TABLE jobs
ADD COLUMN published_commit TEXT;

CREATE INDEX IF NOT EXISTS idx_jobs_source_id
ON jobs(source_id);
2 changes: 2 additions & 0 deletions packages/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"dependencies": {
"@cloudflare/ai-chat": "^0.4.4",
"@cloudflare/codemode": "^0.3.4",
"@cloudflare/shell": "^0.3.2",
"@cloudflare/think": "^0.2.4",
"@cloudflare/worker-bundler": "^0.1.1",
"@cloudflare/workers-oauth-provider": "^0.4.0",
"@epic-web/invariant": "^1.0.0",
"@kody-internal/shared": "file:../shared",
Expand Down
7 changes: 7 additions & 0 deletions packages/worker/src/agent-turn/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { slugifySkillCollectionName } from '#mcp/skills/skill-collections.ts'
import { listUiArtifactsByUserId } from '#mcp/ui-artifacts-repo.ts'
import { listValues } from '#mcp/values/service.ts'
import { runCodemodeWithRegistry } from '#mcp/run-codemode-registry.ts'
import { listJobs } from '#worker/jobs/service.ts'

const defaultSearchLimit = 15
const defaultMaxResponseSize = 4_000
Expand Down Expand Up @@ -61,6 +62,11 @@ export async function createAgentTurnToolSet(input: {
listUiArtifactsByUserId(input.env.APP_DB, userId!, {
hidden: false,
}),
loadJobs: () =>
listJobs({
env: input.env,
userId: userId!,
}),
loadUserSecrets: () =>
listUserSecretsForSearch({
env: input.env,
Expand Down Expand Up @@ -97,6 +103,7 @@ export async function createAgentTurnToolSet(input: {
userId,
skillRows: optionalRows.skillRows,
uiArtifactRows: optionalRows.uiArtifactRows,
jobRows: optionalRows.jobRows,
userSecretRows: optionalRows.userSecretRows,
userValueRows: optionalRows.userValueRows,
appSecretsByAppId,
Expand Down
19 changes: 18 additions & 1 deletion packages/worker/src/app/handlers/saved-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { renderHostedSavedUiHtml } from '#app/saved-ui-hosted-html.ts'
import { type routes } from '#app/routes.ts'
import { createGeneratedUiAppSession } from '#mcp/generated-ui-app-session.ts'
import { getUiArtifactByOwnerIds } from '#mcp/ui-artifacts-repo.ts'
import { resolveSavedAppSource } from '#worker/repo/app-source.ts'

async function getSavedUiForRequest(
request: Request,
Expand Down Expand Up @@ -47,8 +48,24 @@ export function createSavedUiPageHandler(env: Env) {
appId: artifact.id,
homeConnectorId: null,
})
const html = renderHostedSavedUiHtml({
const resolvedArtifact = await resolveSavedAppSource({
env,
baseUrl,
artifact,
})
const html = renderHostedSavedUiHtml({
artifact: {
...artifact,
title: resolvedArtifact.title,
description: resolvedArtifact.description,
hidden: resolvedArtifact.hidden,
parameters: resolvedArtifact.parameters
? JSON.stringify(resolvedArtifact.parameters)
: null,
clientCode: resolvedArtifact.clientCode,
serverCode: resolvedArtifact.serverCode,
serverCodeId: resolvedArtifact.serverCodeId,
},
appSession,
appBaseUrl: baseUrl,
})
Expand Down
1 change: 1 addition & 0 deletions packages/worker/src/env-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export const EnvSchema = object({
CLOUDFLARE_API_TOKEN: optionalNonEmptyStringSchema,
CLOUDFLARE_API_BASE_URL: optionalUrlStringSchema,
CAPABILITY_REINDEX_SECRET: optionalNonEmptyStringSchema,
JOB_REINDEX_SECRET: optionalNonEmptyStringSchema,
HOME_CONNECTOR_SHARED_SECRET: optionalNonEmptyStringSchema,
REMOTE_CONNECTOR_SECRETS: optionalRemoteConnectorSecretsSchema,
})
Expand Down
7 changes: 7 additions & 0 deletions packages/worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AppFacetBridge, AppRunner } from './mcp/app-runner.ts'
import { JobManager } from './jobs/manager-do.ts'
import { StorageRunner } from './storage-runner.ts'
import { AgentTurnRunner } from './agent-turn/runner-do.ts'
import { RepoSession } from './repo/repo-session-do.ts'
import { chatAgentBasePath } from '@kody-internal/shared/chat-routes.ts'
import { getWorkerSentryOptions } from './sentry-options.ts'
import { handleRequest } from '#app/handler.ts'
Expand All @@ -34,6 +35,7 @@ import {
import { readGeneratedUiAppBackendSession } from './mcp/generated-ui-app-auth.ts'
import { withCors } from './utils.ts'
import { handleCapabilityReindexRequest } from './capability-maintenance.ts'
import { handleJobReindexRequest } from './job-maintenance.ts'
import { handleMemoryReindexRequest } from './memory-maintenance.ts'
import { handleSkillReindexRequest } from './skill-maintenance.ts'
import { handleUiArtifactReindexRequest } from './ui-artifact-maintenance.ts'
Expand All @@ -46,6 +48,7 @@ import {
export {
ChatAgent,
AgentTurnRunner,
RepoSession,
CodemodeFetchGateway,
HomeConnectorSession,
HomeMCP,
Expand Down Expand Up @@ -115,6 +118,10 @@ const appHandler = withCors({
return handleUiArtifactReindexRequest(request, env)
}

if (url.pathname === '/__maintenance/reindex-jobs') {
return handleJobReindexRequest(request, env)
}

if (url.pathname === oauthPaths.authorize) {
return handleAuthorizeRequest(request, env)
}
Expand Down
31 changes: 31 additions & 0 deletions packages/worker/src/job-maintenance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { reindexJobVectors } from './jobs/job-reindex.ts'

export async function handleJobReindexRequest(
request: Request,
env: Env,
): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 })
}

const secret = env.JOB_REINDEX_SECRET?.trim()
if (!secret) {
return new Response('Job reindex is not configured', { status: 503 })
}

const auth = request.headers.get('Authorization')?.trim()
const token = auth?.startsWith('Bearer ')
? auth.slice('Bearer '.length).trim()
: null
if (token !== secret) {
return new Response('Unauthorized', { status: 401 })
}

try {
const { upserted } = await reindexJobVectors(env)
return Response.json({ ok: true, upserted })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return Response.json({ ok: false, error: message }, { status: 500 })
}
}
72 changes: 72 additions & 0 deletions packages/worker/src/jobs/job-reindex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {
getCapabilityVectorIndex,
isCapabilitySearchOffline,
} from '#mcp/capabilities/capability-search.ts'
import { buildJobEmbedText } from '#mcp/jobs-embed.ts'
import { jobVectorId } from '#mcp/jobs-vectorize.ts'
import { listJobs } from './service.ts'

const upsertBatchSize = 16

type JobEmbeddingBatchResult = { data?: Array<Array<number>> }

export async function reindexJobVectors(
env: Env,
): Promise<{ upserted: number }> {
const index = getCapabilityVectorIndex(env)
if (!index) {
throw new Error('CAPABILITY_VECTOR_INDEX binding is not configured')
}
if (isCapabilitySearchOffline(env)) {
return { upserted: 0 }
}

const rows = await env.APP_DB.prepare(
`SELECT DISTINCT user_id FROM jobs WHERE source_id IS NOT NULL OR code IS NOT NULL`,
).all<{ user_id: string }>()
const userIds = (rows.results ?? []).map((row) => row.user_id).filter(Boolean)
if (userIds.length === 0) return { upserted: 0 }

const jobs = (
await Promise.all(
userIds.map((userId) =>
listJobs({
env,
userId,
}).then((userJobs) => userJobs.map((job) => ({ userId, job }))),
),
)
).flat()
if (jobs.length === 0) return { upserted: 0 }

let upserted = 0
for (let offset = 0; offset < jobs.length; offset += upsertBatchSize) {
const batch = jobs.slice(offset, offset + upsertBatchSize)
const texts = batch.map(({ job }) =>
buildJobEmbedText({
name: job.name,
scheduleSummary: job.scheduleSummary,
sourceId: job.sourceId,
publishedCommit: job.publishedCommit,
}),
Comment thread
cursor[bot] marked this conversation as resolved.
)
Comment on lines +45 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm buildJobEmbedText's expected inputs and how the primary write path calls it.
fd -t f 'jobs-embed.ts' | xargs -I{} sed -n '1,120p' {}
rg -nP -C3 '\bbuildJobEmbedText\s*\('

Repository: kentcdodds/kody

Length of output: 3613


🏁 Script executed:

#!/bin/bash
# Check the Job type definition to see if description field exists
rg -nP 'type Job|interface Job' --max-count=10
rg -nP '\bjob\.description\b'
# Also check if any entity has a description field
rg -nP '\bdescription:' packages/worker/src/jobs/types.ts -A2 -B2

Repository: kentcdodds/kody

Length of output: 3031


description is wired to job.name — embedding input is wrong.

description: job.name duplicates the name into the description slot. Job objects have a description field that should be used instead. This corrupts embeddings across all reindex, create, and update paths, causing search quality to degrade silently.

🐛 Proposed fix
 		const texts = batch.map(({ job }) =>
 			buildJobEmbedText({
 				name: job.name,
-				description: job.name,
+				description: job.description,
 				scheduleSummary: job.scheduleSummary,
 				sourceId: job.sourceId,
 				publishedCommit: job.publishedCommit,
 			}),
 		)

Note: The same bug exists in packages/worker/src/jobs/service.ts (lines 196 and 315) and packages/worker/src/mcp/capabilities/unified-search.ts (line 391). All call sites pass entity name to description instead of the actual description field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/jobs/job-reindex.ts` around lines 45 - 53, The embedding
input incorrectly sets description: job.name when calling buildJobEmbedText in
job-reindex.ts; change it to description: job.description so the actual job
description is embedded (also search for other call sites like the
buildJobEmbedText calls in jobs/service.ts and
mcp/capabilities/unified-search.ts where description is similarly passed as
job.name and fix them to use job.description). Ensure you update every
invocation of buildJobEmbedText that currently uses job.name for the description
parameter so embeddings reflect the true description field.

const result = (await env.AI.run('@cf/baai/bge-small-en-v1.5', {
text: texts,
pooling: 'mean',
})) as JobEmbeddingBatchResult
const vectors = result.data
if (!vectors || vectors.length !== batch.length) {
throw new Error('Workers AI embedding batch size mismatch for jobs')
}
await index.upsert(
batch.map(({ userId, job }, index_) => ({
id: jobVectorId(job.id),
values: vectors[index_]!,
metadata: { kind: 'job', userId },
})),
)
upserted += batch.length
}

return { upserted }
}
Loading
Loading