fix: stabilize Codex and development dependencies (#798)

* fix: stabilize dependency and Codex integration

Resolve development-only dependency advisories, remove the search test teardown race, and validate the Codex 0.144.1 event and health contracts.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* chore: keep generated lockfile reviewable

Exclude pnpm-lock.yaml from Prettier and restore pnpm's generated formatting after dependency resolution.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* test: stabilize Codex process lifecycle

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Brad Groux 2026-07-10 00:03:27 -05:00 committed by GitHub
parent 84ace3e9de
commit b3eda417ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 429 additions and 630 deletions

1
.prettierignore Normal file
View file

@ -0,0 +1 @@
pnpm-lock.yaml

View file

@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Updated the Codex SDK integration to `0.144.1` and pinned patched transitive
development-tool dependencies (#792, #795).
### Fixed
- Isolated QMD result normalization coverage from unrelated persistent search
collections and restored test environment state only after temporary search
roots are removed, eliminating the intermittent teardown race (#793).
## [5.2.1] - 2026-06-29
### Fixed

View file

@ -88,6 +88,7 @@ This file defines project-specific rules, context, and lessons learned for AI ag
- ❌ Used wrong field in backfilled events (`status: "success"` vs `success: true`)
- ✅ Match actual runtime schema exactly in test fixtures
- ✅ Keep `pnpm-lock.yaml` generated by pnpm; do not format it with Prettier
---

View file

@ -167,7 +167,10 @@ Settings exposes Codex readiness through a dedicated health check:
GET /api/settings/codex/health
```
The response reports Codex CLI install/version/auth state, SDK import availability, Codex agent profile readiness, enabled Codex profiles, and recommendations.
The response reports Codex CLI install/version/auth state, the installed Codex
SDK version and import availability, Codex agent profile readiness, enabled
Codex profiles, and recommendations. Veritas Kanban currently validates its
stream adapter against `@openai/codex-sdk` 0.144.1 event contracts.
## MCP And Project Instructions

792
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -14,9 +14,12 @@ allowBuilds:
esbuild: true
overrides:
'@babel/core@<7.29.1': 7.29.7
'@modelcontextprotocol/sdk>express-rate-limit': 8.5.2
'@xmldom/xmldom': 0.8.13
'esbuild@<0.28.1': 0.28.1
fast-uri: '>=3.1.2'
'form-data@<4.0.6': 4.0.6
hono: '>=4.12.18'
ip-address: '>=10.1.1'
postcss: '>=8.5.10'
@ -25,3 +28,5 @@ overrides:
minimatch: '>=10.2.3'
path-to-regexp: '>=8.4.0'
tmp: '>=0.2.6'
'undici@>=6.0.0 <7.0.0': 6.27.0
'undici@>=7.0.0 <8.0.0': 7.28.0

View file

@ -16,7 +16,7 @@
"reset-password": "tsx src/scripts/reset-password.ts"
},
"dependencies": {
"@openai/codex-sdk": "0.140.0",
"@openai/codex-sdk": "0.144.1",
"@veritas-kanban/shared": "workspace:*",
"ajv": "^8.20.0",
"bcrypt": "^6.0.0",

View file

@ -85,6 +85,7 @@ vi.mock('../services/circuit-registry.js', () => ({
}));
import { AgentReadinessError, ClawdbotAgentService } from '../services/clawdbot-agent-service.js';
import type { ThreadEvent } from '@openai/codex-sdk';
import type { AgentConfig, Task } from '@veritas-kanban/shared';
const fixtureDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures', 'codex');
@ -109,7 +110,7 @@ function testableService(tmpDir: string): TestableClawdbotAgentService {
return service;
}
function createFakeChild(fixturePath: string, exitCode = 0) {
function createFakeChild(fixture: string, exitCode = 0) {
const child = new EventEmitter() as EventEmitter & {
stdout: PassThrough;
stderr: PassThrough;
@ -127,10 +128,11 @@ function createFakeChild(fixturePath: string, exitCode = 0) {
return true;
});
setImmediate(async () => {
child.stdout.write(await fs.readFile(fixturePath, 'utf-8'));
child.stdout.end();
setTimeout(() => child.emit('close', exitCode, null), 10);
queueMicrotask(() => {
child.stdout.once('end', () => {
child.emit('close', exitCode, null);
});
child.stdout.end(fixture);
});
return child;
@ -158,7 +160,7 @@ function createControllableChild() {
async function waitFor(assertion: () => void): Promise<void> {
const started = Date.now();
let lastError: unknown;
while (Date.now() - started < 3000) {
while (Date.now() - started < 10_000) {
try {
assertion();
return;
@ -245,85 +247,93 @@ describe('ClawdbotAgentService Codex providers', () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
it('runs the Codex CLI adapter against mocked JSONL and records telemetry', async () => {
mockSpawn.mockReturnValue(createFakeChild(path.join(fixtureDir, 'success.jsonl')));
const service = testableService(tmpDir);
it(
'runs the Codex CLI adapter against mocked JSONL and records telemetry',
{ timeout: 20_000 },
async () => {
const fixture = await fs.readFile(path.join(fixtureDir, 'success.jsonl'), 'utf-8');
mockSpawn.mockReturnValue(createFakeChild(fixture));
const service = testableService(tmpDir);
const status = await service.startAgent(task.id, 'codex');
const status = await service.startAgent(task.id, 'codex');
expect(status.status).toBe('running');
expect(mockSpawn).toHaveBeenCalledWith(
'codex',
expect.arrayContaining(['exec', '--json', '--sandbox', 'workspace-write']),
expect.objectContaining({ cwd: tmpDir, shell: false })
);
await waitFor(() => {
expect(mockUpdateTask).toHaveBeenCalledWith(
task.id,
expect.objectContaining({ status: 'done' })
expect(status.status).toBe('running');
expect(mockSpawn).toHaveBeenCalledWith(
'codex',
expect.arrayContaining(['exec', '--json', '--sandbox', 'workspace-write']),
expect.objectContaining({ cwd: tmpDir, shell: false })
);
});
expect(mockTelemetryEmit).toHaveBeenCalledWith(
expect.objectContaining({
type: 'run.tokens',
inputTokens: 12,
outputTokens: 8,
totalTokens: 20,
model: 'gpt-5.5',
})
);
await waitFor(() => {
expect(mockLogActivity).toHaveBeenCalledWith(
'agent_completed',
task.id,
task.title,
expect.objectContaining({ provider: 'codex-cli', success: true }),
'codex'
await waitFor(() => {
expect(mockUpdateTask).toHaveBeenCalledWith(
task.id,
expect.objectContaining({ status: 'done' })
);
});
expect(mockTelemetryEmit).toHaveBeenCalledWith(
expect.objectContaining({
type: 'run.tokens',
inputTokens: 12,
outputTokens: 8,
totalTokens: 20,
model: 'gpt-5.5',
})
);
});
expect(mockStartStep).toHaveBeenCalledWith(
expect.any(String),
'stream',
expect.objectContaining({
eventType: 'stream.stdout',
stream: 'stdout',
provider: 'codex-cli',
chunkBytes: expect.any(Number),
})
);
expect(mockStartStep).toHaveBeenCalledWith(
expect.any(String),
'finalize',
expect.objectContaining({
eventType: 'run.finalizing',
exitCode: 0,
signal: null,
success: true,
provider: 'codex-cli',
})
);
expect(mockStartStep).toHaveBeenCalledWith(
expect.any(String),
'complete',
expect.objectContaining({
eventType: 'turn.completed',
totalTokens: 20,
model: 'gpt-5.5',
finalResult: 'Codex completed the task.',
})
);
expect(mockStartStep).toHaveBeenCalledWith(
expect.any(String),
'complete',
expect.objectContaining({
eventType: 'run.completed',
success: true,
provider: 'codex-cli',
model: 'gpt-5.5',
})
);
});
await waitFor(() => {
expect(mockLogActivity).toHaveBeenCalledWith(
'agent_completed',
task.id,
task.title,
expect.objectContaining({ provider: 'codex-cli', success: true }),
'codex'
);
});
await waitFor(() => {
expect(service.getAgentStatus(task.id)).toBeNull();
});
expect(mockStartStep).toHaveBeenCalledWith(
expect.any(String),
'stream',
expect.objectContaining({
eventType: 'stream.stdout',
stream: 'stdout',
provider: 'codex-cli',
chunkBytes: expect.any(Number),
})
);
expect(mockStartStep).toHaveBeenCalledWith(
expect.any(String),
'finalize',
expect.objectContaining({
eventType: 'run.finalizing',
exitCode: 0,
signal: null,
success: true,
provider: 'codex-cli',
})
);
expect(mockStartStep).toHaveBeenCalledWith(
expect.any(String),
'complete',
expect.objectContaining({
eventType: 'turn.completed',
totalTokens: 20,
model: 'gpt-5.5',
finalResult: 'Codex completed the task.',
})
);
expect(mockStartStep).toHaveBeenCalledWith(
expect.any(String),
'complete',
expect.objectContaining({
eventType: 'run.completed',
success: true,
provider: 'codex-cli',
model: 'gpt-5.5',
})
);
}
);
it('maps Codex file events to task deliverables linked to the attempt', async () => {
const service = testableService(tmpDir);
@ -359,6 +369,7 @@ describe('ClawdbotAgentService Codex providers', () => {
})
);
});
expect(mockStartStep).toHaveBeenCalledWith(
'attempt_fixture',
'execute',
@ -377,6 +388,47 @@ describe('ClawdbotAgentService Codex providers', () => {
);
});
it('accepts every Codex SDK 0.144 event contract consumed by the stream adapter', () => {
const service = testableService(tmpDir);
const logPath = path.join(tmpDir, 'codex.md');
const events = [
{ type: 'thread.started', thread_id: 'thread_fixture' },
{ type: 'turn.started' },
{
type: 'turn.completed',
usage: {
input_tokens: 10,
cached_input_tokens: 2,
output_tokens: 5,
reasoning_output_tokens: 1,
},
},
{ type: 'turn.failed', error: { message: 'fixture failure' } },
{
type: 'item.started',
item: { id: 'item_started', type: 'agent_message', text: 'starting' },
},
{
type: 'item.updated',
item: { id: 'item_updated', type: 'agent_message', text: 'working' },
},
{
type: 'item.completed',
item: { id: 'item_completed', type: 'agent_message', text: 'finished' },
},
{ type: 'error', message: 'fixture stream error' },
] satisfies ThreadEvent[];
const parsed = events.map((event) => service.handleCodexEvent(event, logPath));
expect(parsed[2]?.usage).toEqual({
inputTokens: 10,
outputTokens: 5,
totalTokens: 15,
});
expect(parsed[6]?.summary).toBe('finished');
});
it('classifies streamed output, retry, and abort lifecycle events in traces', async () => {
const service = testableService(tmpDir);
const logPath = path.join(tmpDir, 'codex.md');
@ -487,7 +539,8 @@ describe('ClawdbotAgentService Codex providers', () => {
await expect(service.startAgent(task.id, 'codex')).rejects.toBeInstanceOf(AgentReadinessError);
mockSpawn.mockReturnValue(createFakeChild(path.join(fixtureDir, 'success.jsonl')));
const fixture = await fs.readFile(path.join(fixtureDir, 'success.jsonl'), 'utf-8');
mockSpawn.mockReturnValue(createFakeChild(fixture));
const status = await service.startAgent(task.id, 'codex', {
overrideReason: 'Maintainer approved urgent fix',
@ -514,5 +567,8 @@ describe('ClawdbotAgentService Codex providers', () => {
expect.objectContaining({ status: 'done' })
);
});
await waitFor(() => {
expect(service.getAgentStatus(task.id)).toBeNull();
});
});
});

View file

@ -156,7 +156,7 @@ describe('Settings Codex health route', () => {
mockCodexHealthService.getHealth.mockResolvedValue({
checkedAt: '2026-05-06T00:00:00.000Z',
cli: { installed: true, authenticated: true, version: 'codex-cli 0.128.0' },
sdk: { available: true },
sdk: { available: true, version: '0.144.1' },
agents: { codexCli: true, codexSdk: true, codexCloud: true, enabled: ['codex'] },
ready: { cli: true, sdk: true, cloud: true, overall: true },
recommendations: [],
@ -166,6 +166,7 @@ describe('Settings Codex health route', () => {
expect(response.status).toBe(200);
expect(response.body.ready.overall).toBe(true);
expect(response.body.sdk.version).toBe('0.144.1');
expect(mockCodexHealthService.getHealth).toHaveBeenCalled();
});

View file

@ -36,8 +36,8 @@ describe('SearchService', () => {
});
afterEach(async () => {
process.env = oldEnv;
await fs.rm(root, { recursive: true, force: true });
process.env = oldEnv;
});
it('searches task and docs markdown with keyword fallback', async () => {
@ -135,7 +135,10 @@ describe('SearchService', () => {
);
});
const result = await new SearchService().search({ query: 'semantic search' });
const result = await new SearchService().search({
query: 'semantic search',
collections: ['tasks-active'],
});
expect(result.backend).toBe('qmd');
expect(result.degraded).toBe(false);

View file

@ -1,4 +1,5 @@
import { execFile } from 'child_process';
import { readFile } from 'fs/promises';
import { promisify } from 'util';
import { ConfigService } from './config-service.js';
@ -16,6 +17,7 @@ export interface CodexHealthStatus {
};
sdk: {
available: boolean;
version?: string;
error?: string;
};
agents: {
@ -107,7 +109,15 @@ export class CodexHealthService {
private async checkSdk(): Promise<CodexHealthStatus['sdk']> {
try {
await import('@openai/codex-sdk');
return { available: true };
const moduleUrl = import.meta.resolve('@openai/codex-sdk');
const packageUrl = new URL('../package.json', moduleUrl);
const packageMetadata = JSON.parse(await readFile(packageUrl, 'utf8')) as {
version?: unknown;
};
return {
available: true,
version: typeof packageMetadata.version === 'string' ? packageMetadata.version : undefined,
};
} catch (error: any) {
return { available: false, error: error.message || 'Codex SDK is not available' };
}