Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
14 changes: 10 additions & 4 deletions docs/use/execute.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@ relevant saved package with **`entity: "{kody_id}:package"`**.

## Saved packages

Saved packages and one-off **execute** code share the same module-oriented
runtime model:
Saved packages, scheduled jobs, and one-off **execute** code share the same
module-oriented runtime model:

- saved packages persist repo-backed source rooted at `package.json`
- package exports are defined by standard `package.json.exports`
- package-specific metadata lives under `package.json#kody`
- package jobs are schedules declared under `package.json#kody.jobs`
- package apps are optional UI surfaces declared under `package.json#kody.app`
- non-package jobs can also be scheduled directly with
**`codemode.job_schedule(...)`** without creating a saved package
- **`codemode.job_schedule_once(...)`** remains available as a convenience alias
for one-off schedules

When you need to edit saved source, prefer the repo-backed workflow in
[Repo-backed editing sessions](./repo-sessions.md). Open by package identity
Expand All @@ -70,9 +74,11 @@ Typical pattern inside execute:

## Storage

Kody supports durable storage binding for execute and package-owned jobs.
Kody supports durable storage binding for execute and scheduled jobs,
including package-owned jobs and non-package jobs created with
`job_schedule` or `job_schedule_once`.

- bound storage is app- or package-owned durable state
- bound storage is execute-, app-, package-, or job-owned durable state
- import **`storage`** from **`kody:runtime`**
- use **`storage.get(...)`**, **`storage.set(...)`**, **`storage.list(...)`**,
and **`storage.sql(query, params?)`**
Expand Down
8 changes: 5 additions & 3 deletions docs/use/first-steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ connector, value, or secret reference, then run work through **execute**.
- **Pass `memoryContext`** when durable user memory may matter. Kody uses it to
surface a small set of relevant long-term memories that have not already been
shown in the same conversation.
- **Think in packages.** A saved package is the only top-level persisted
primitive. Packages can expose exports, declare package-owned jobs, and
optionally expose an app/UI surface.
- **Think in packages for reusable saved code.** Packages expose exports,
declare package-owned jobs, and can optionally expose an app/UI surface.
For scheduled work that should not become a saved package, use the built-in
`job_schedule` capability. `job_schedule_once` remains available as a
one-off convenience alias.
- **Ask for natural-language goals**, for example: “Search Kody for GitHub pull
request automation” or “Find a saved package for Cloudflare DNS helpers.”
- **Do not paste secrets in chat.** Use saved secrets, generated UI, or the
Expand Down
6 changes: 5 additions & 1 deletion docs/use/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,18 @@ Treat package apps like Worker-style modules:

## Package-owned jobs

Jobs belong to packages.
Packages can own jobs, but packages are no longer the only scheduling path.

- Define them under `package.json#kody.jobs`
- Reference package-local entry modules
- Treat their runtime state as package-owned implementation detail

Jobs are part of the package definition.

For repo-backed jobs that are not part of a saved package, use
`job_schedule` instead. `job_schedule_once` remains available as the one-off
shortcut.

## Save and edit packages

Use:
Expand Down
181 changes: 181 additions & 0 deletions packages/worker/src/jobs/service.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,187 @@ test('executeJobOnce binds scheduled jobs to writable storage', async () => {
}
})

test('executeJobOnce runs repo-backed one-off jobs from kody.json manifests', async () => {
const db = createDatabase()
const env = {
APP_DB: db,
LOADER: {} as WorkerLoader,
REPO_SESSION: {} as DurableObjectNamespace,
STORAGE_RUNNER: {
idFromName(name: string) {
return name as unknown as DurableObjectId
},
get() {
return {
getValue: async () => ({ key: 'count', value: 2 }),
setValue: async () => ({ ok: true, key: 'count' }),
deleteValue: async () => ({ ok: true, key: 'count', deleted: true }),
clearStorage: async () => ({ ok: true }),
listValues: async () => ({
entries: [],
estimatedBytes: 0,
truncated: false,
nextStartAfter: null,
pageSize: 250,
}),
exportStorage: async () => ({
entries: [],
estimatedBytes: 0,
truncated: false,
nextStartAfter: null,
pageSize: 250,
}),
sqlQuery: async () => ({
columns: ['value'],
rows: [{ value: 2 }],
rowCount: 1,
rowsRead: 1,
rowsWritten: 0,
}),
}
},
},
} as unknown as Env
mockRepoPersistence()
const callerContext = createBaseCallerContext()

const jobView = await createJob({
env,
callerContext,
body: {
name: 'Capability-created one-off job',
code: 'export default async () => ({ ok: true, adHoc: true })',
params: {
step: 'lights-off',
},
schedule: {
type: 'once',
runAt: '2026-04-17T15:00:00Z',
},
},
})
Comment on lines +754 to +812

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 | 🟡 Minor

Missing mockRepoPersistence() before createJob — test relies on state leaked from the previous test.

Unlike the adjacent test on line 594 (which calls mockRepoPersistence() at line 635 before createJob), this new test invokes createJob at line 798 without first configuring repoMockModule.ensureEntitySource / syncArtifactSourceSnapshot. Currently it only works because the mockImplementation set by an earlier test run is still in place (vi.restoreAllMocks() in afterEach does not reset implementations on non-spy vi.fn() mocks). Running this test in isolation, or reordering tests, will break it because ensureEntitySource would return undefined and ensuredSource.id in createJob would throw.

🛠️ Suggested fix
 	} as unknown as Env
+	mockRepoPersistence()
 	const callerContext = createBaseCallerContext()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/jobs/service.node.test.ts` around lines 754 - 812, The
test calls createJob but never sets up the repo mocks, causing
repoMockModule.ensureEntitySource / syncArtifactSourceSnapshot to be undefined
when run in isolation; add a call to mockRepoPersistence() before createJob in
this test so the repoMockModule implementations (ensureEntitySource returning an
ensuredSource with an id) are registered; target the test block using createJob
and ensure mockRepoPersistence() is invoked earlier in that test so createJob's
access to ensuredSource.id won't throw.


const executeSpy = vi
.spyOn(
await import('#mcp/run-codemode-registry.ts'),
'runBundledModuleWithRegistry',
)
.mockResolvedValue({
result: {
ok: true,
adHoc: true,
},
logs: ['ad hoc job executed'],
})

try {
const sessionClient = {
openSession: vi.fn(async () => ({
id: `job-runtime-${jobView.id}`,
source_id: jobView.sourceId,
source_root: '/',
base_commit: 'published-commit-1',
session_repo_id: 'session-repo-ad-hoc',
session_repo_name: 'session-repo-name',
session_repo_namespace: 'default',
conversation_id: null,
last_checkpoint_commit: null,
last_check_run_id: null,
last_check_tree_hash: null,
expires_at: null,
created_at: '2026-04-16T00:00:00.000Z',
updated_at: '2026-04-16T00:00:00.000Z',
published_commit: 'published-commit-1',
manifest_path: 'kody.json',
entity_type: 'job' as const,
})),
runChecks: vi.fn(),
readFile: vi.fn(async ({ path }: { path: string }) => ({
path,
content:
path === 'kody.json'
? JSON.stringify({
version: 1,
kind: 'job',
title: 'Capability-created one-off job',
description: 'Runs once at 2026-04-17T15:00:00.000Z',
sourceRoot: '/',
entrypoint: 'src/job.ts',
})
: 'export default async () => ({ ok: true, adHoc: true })',
})),
tree: vi.fn(async () => ({
path: '',
name: '',
type: 'directory' as const,
size: 0,
children: [
{
path: 'kody.json',
name: 'kody.json',
type: 'file' as const,
size: 1,
},
{
path: 'src/job.ts',
name: 'job.ts',
type: 'file' as const,
size: 1,
},
],
})),
discardSession: vi.fn(),
}
const repoSessionRpcSpy = vi
.spyOn(await import('#worker/repo/repo-session-do.ts'), 'repoSessionRpc')
.mockReturnValue(sessionClient as never)
const row = await (
await import('./repo.ts')
).getJobRowById(db, callerContext.user.userId, jobView.id)
if (!row) {
throw new Error('Expected created job row.')
}
const outcome = await executeJobOnce({
env,
job: row.record,
callerContext,
})

expect(outcome.execution).toEqual({
ok: true,
result: {
ok: true,
adHoc: true,
},
logs: ['ad hoc job executed'],
})
expect(sessionClient.runChecks).not.toHaveBeenCalled()
expect(sessionClient.readFile).toHaveBeenCalledWith({
sessionId: `job-runtime-${jobView.id}`,
userId: 'user-123',
path: 'kody.json',
})
expect(executeSpy).toHaveBeenCalledTimes(1)
expect(executeSpy.mock.calls[0]?.[1]).toMatchObject({
repoContext: expect.objectContaining({
entityKind: 'job',
entityId: jobView.id,
manifestPath: 'kody.json',
}),
})
expect(executeSpy.mock.calls[0]?.[4]).toMatchObject({
storageTools: {
userId: 'user-123',
storageId: `job:${jobView.id}`,
writable: true,
},
})
repoSessionRpcSpy.mockRestore()
} finally {
executeSpy.mockRestore()
}
})

test('executeJobOnce preserves codemode secret and value semantics', async () => {
const db = createDatabase()
const env = {
Expand Down
Loading
Loading