Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions .github/skills/coc-knowledge/references/ralph.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,29 @@ The endpoint:
The SPA shows a **"Promote to Ralph"** pill in the follow-up area for eligible
chats and calls this endpoint via `coc-client`'s `processes.promoteToRalph`
helper.

## Resume Routes

Session resume endpoints share infrastructure in
`packages/coc/src/server/routes/ralph-route-utils.ts`.
`/continue` and `/new-loop` both use it for in-flight Ralph task scans,
`additionalIterations` validation/default resolution, resume hard caps, and
best-effort recovery of `workingDirectory` / `folderPath` from the latest
iteration process. Final-check gap-fix loops use the same additional-iteration
resolver so per-repo `maxRalphIterations` fallback stays consistent.

## Final Check Automation

`orchestrateFinalCheck(...)` in
`packages/coc/src/server/ralph/orchestrate-final-check.ts` appends the
final-check result to `progress.md`, reads the session once, and persists a
`RalphFinalCheckRecord` with shared base fields (`loopIndex`,
`sourceIteration`, `taskId`, `processId`, `startedAt`, `completedAt`) plus
outcome-specific metadata.

Terminal paths broadcast `ralph-session-complete`: clean checks use
`reason='signal'`, cap-reached checks use `reason='cap'`, parse failures use
`reason='final-check-failed'`, gap-loop creation failures use
`reason='final-check-gap-loop-start-failed'`, and gap-loop enqueue failures use
`reason='final-check-gap-enqueue-failed'`. A successful gap-fix enqueue does
not broadcast completion because the next loop continues the session.
3 changes: 3 additions & 0 deletions packages/coc-agent-sdk/src/codex-sdk-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ interface CodexStartThreadOptions {
sandboxMode?: 'read-only' | 'workspace-write' | 'danger-full-access';
approvalPolicy?: 'never' | 'on-request' | 'on-failure' | 'untrusted';
networkAccessEnabled?: boolean;
/** Reasoning level passed to the Codex backend (e.g. 'low', 'medium', 'high', 'xhigh'). */
reasoningLevel?: string;
}

interface CodexCatalogModel {
Expand Down Expand Up @@ -802,6 +804,7 @@ export class CodexSDKService implements ISDKService {
...(model ? { model } : {}),
...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),
...(options.skillDirectories?.length ? { additionalDirectories: options.skillDirectories } : {}),
...(options.reasoningEffort ? { reasoningLevel: options.reasoningEffort } : {}),
skipGitRepoCheck: true,
approvalPolicy: 'never',
sandboxMode: 'danger-full-access',
Expand Down
2 changes: 2 additions & 0 deletions packages/coc-client/src/contracts/work-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,4 +272,6 @@ export interface WorkItemTreeFilter {
type?: WorkItemType;
status?: WorkItemStatus;
includeArchived?: boolean;
/** When true, items with status "done" are included. Defaults to false. */
includeDone?: boolean;
}
1 change: 1 addition & 0 deletions packages/coc-client/src/domains/work-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export class WorkItemsClient {
if (filter?.type) query.type = filter.type;
if (filter?.status) query.status = filter.status;
if (filter?.includeArchived !== undefined) query.includeArchived = filter.includeArchived;
if (filter?.includeDone !== undefined) query.includeDone = filter.includeDone;
return this.transport.request<WorkItemTreeResponse>(path(workspaceId, '/tree'), { query });
}
}
12 changes: 10 additions & 2 deletions packages/coc/src/server/executors/chat-base-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,11 +518,19 @@ export abstract class ChatBaseExecutor extends BaseExecutor {
effectiveModel = resolveDefaultModel(this.dataDir, payload.workspaceId, defaultModelMode);
}

// Resolve reasoning effort: explicit task config > persisted per-model preference > SDK default
// Resolve reasoning effort:
// explicit task config
// > provider-scoped persisted default (cfg.models.providers[provider].reasoningEfforts)
// > global persisted default — Copilot legacy only (cfg.models.reasoningEfforts)
// > SDK default (model catalog default, then FALLBACK_REASONING_EFFORT_ORDER)
let requestedEffort: Parameters<typeof resolveReasoningSelection>[0]['requestedEffort'] = task.config.reasoningEffort;
if (!requestedEffort && effectiveModel) {
const cfg = loadConfigFile();
const persisted = cfg?.models?.reasoningEfforts?.[effectiveModel];
const providerSettings = cfg?.models?.providers?.[taskProvider];
const effortMap: Record<string, string> = providerSettings
? (providerSettings.reasoningEfforts ?? {})
: (taskProvider === 'copilot' ? (cfg?.models?.reasoningEfforts ?? {}) : {});
const persisted = effortMap[effectiveModel];
if (persisted) requestedEffort = persisted as NonNullable<typeof requestedEffort>;
}
const reasoningSelection = resolveReasoningSelection({
Expand Down
16 changes: 10 additions & 6 deletions packages/coc/src/server/executors/follow-up-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,17 +299,21 @@ export class FollowUpExecutor extends ChatBaseExecutor {
const { resolveDefaultModel } = await import('../preferences-handler');
reasoningModel = resolveDefaultModel(this.dataDir, wsId, 'followUp');
}
// Resolve reasoning effort: per-turn override > persisted per-model preference > SDK default.
// The per-turn override is supplied by the UI's EffortPillSelector
// (or any caller of executeFollowUp) and wins over the user's
// persisted default — this matches the chat-base executor's
// precedence so new chats and follow-ups behave identically.
// Resolve reasoning effort:
// per-turn override (from EffortPillSelector)
// > provider-scoped persisted default (cfg.models.providers[provider].reasoningEfforts)
// > global persisted default — Copilot legacy only (cfg.models.reasoningEfforts)
// > SDK default (model catalog default, then FALLBACK_REASONING_EFFORT_ORDER)
type _RequestedEffort = Parameters<typeof resolveReasoningSelection>[0]['requestedEffort'];
let requestedEffort: _RequestedEffort = reasoningEffort;
if (!requestedEffort && reasoningModel) {
const { loadConfigFile } = await import('../../config');
const cfg = loadConfigFile();
const persisted = cfg?.models?.reasoningEfforts?.[reasoningModel];
const providerSettings = cfg?.models?.providers?.[sessionProvider];
const effortMap: Record<string, string> = providerSettings
? (providerSettings.reasoningEfforts ?? {})
: (sessionProvider === 'copilot' ? (cfg?.models?.reasoningEfforts ?? {}) : {});
const persisted = effortMap[reasoningModel];
if (persisted) requestedEffort = persisted as _RequestedEffort;
}
const reasoningSelection = resolveReasoningSelection({
Expand Down
68 changes: 21 additions & 47 deletions packages/coc/src/server/ralph/orchestrate-final-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { RalphSessionStore } from './ralph-session-store';
import { buildRalphIterationTask } from './enqueue-iteration';
import type { RalphFinalCheckRecord } from './types';
import { getLogger, LogCategory } from '@plusplusoneplusplus/forge';
import { RALPH_DEFAULT_MAX_ITERATIONS, readRepoPreferences } from '../preferences-handler';
import { resolveRalphAdditionalIterations } from '../routes/ralph-route-utils';

// ============================================================================
// Injected dependencies (allow testability without a live bridge/WS)
Expand Down Expand Up @@ -101,18 +101,17 @@ export async function orchestrateFinalCheck(input: OrchestrateFinalCheckInput):

// ── 3. Determine result and decide next action ───────────────────────────
const session = await store.readSessionRecord(workspaceId, sessionId);
const startedAt = session?.finalChecks?.find(c => c.checkIndex === checkIndex)?.startedAt ?? nowIso;
const baseCheckRecord = buildBaseCheckRecord({ loopIndex, sourceIteration, taskId, processId }, startedAt, nowIso);

if (parsed.status === 'unparseable' || parsed.status === 'invalid') {
// Unparseable or contradictory response — record as failed, do not start gap loop
logger.warn(LogCategory.AI, `[Ralph/FinalCheck] Check ${checkIndex} ${parsed.status} for ${sessionId}: ${parsed.error ?? ''}`);
await safeUpsertRecord(store, workspaceId, sessionId, checkIndex, {
status: 'failed',
loopIndex,
sourceIteration,
taskId,
processId,
startedAt: session?.finalChecks?.find(c => c.checkIndex === checkIndex)?.startedAt ?? nowIso,
completedAt: nowIso,
...baseCheckRecord,
hasGaps: false,
gapCount: 0,
}, logger);
broadcastSessionComplete({ workspaceId, sessionId, processId, totalIterations: sourceIteration, reason: 'final-check-failed' });
return;
Expand All @@ -122,12 +121,7 @@ export async function orchestrateFinalCheck(input: OrchestrateFinalCheckInput):
// ── Clean result ─────────────────────────────────────────────────────
await safeUpsertRecord(store, workspaceId, sessionId, checkIndex, {
status: 'completed',
loopIndex,
sourceIteration,
taskId,
processId,
startedAt: session?.finalChecks?.find(c => c.checkIndex === checkIndex)?.startedAt ?? nowIso,
completedAt: nowIso,
...baseCheckRecord,
hasGaps: false,
gapCount: 0,
gapLoopStarted: false,
Expand All @@ -144,12 +138,7 @@ export async function orchestrateFinalCheck(input: OrchestrateFinalCheckInput):
logger.debug(LogCategory.AI, `[Ralph/FinalCheck] Cap reached (${existingGapLoops}/${maxGapFixLoops}) for session ${sessionId}; stopping automation.`);
await safeUpsertRecord(store, workspaceId, sessionId, checkIndex, {
status: 'completed',
loopIndex,
sourceIteration,
taskId,
processId,
startedAt: session?.finalChecks?.find(c => c.checkIndex === checkIndex)?.startedAt ?? nowIso,
completedAt: nowIso,
...baseCheckRecord,
hasGaps: true,
gapCount: parsed.gaps.length,
gapLoopStarted: false,
Expand All @@ -168,16 +157,7 @@ export async function orchestrateFinalCheck(input: OrchestrateFinalCheckInput):
logger.warn(LogCategory.AI, `[Ralph/FinalCheck] No gapFixGoal for ${sessionId} — falling back to gap titles.`);
}

// Resolve additional iterations from preferences or default.
let additionalIterations = RALPH_DEFAULT_MAX_ITERATIONS;
if (dataDir) {
try {
const prefs = readRepoPreferences(dataDir, workspaceId);
if (prefs.maxRalphIterations) additionalIterations = prefs.maxRalphIterations;
} catch {
// Preferences are optional
}
}
const additionalIterations = resolveRalphAdditionalIterations(undefined, dataDir, workspaceId);

let newLoopRecord;
try {
Expand All @@ -187,12 +167,7 @@ export async function orchestrateFinalCheck(input: OrchestrateFinalCheckInput):
// Record that a gap loop was intended but failed to start.
await safeUpsertRecord(store, workspaceId, sessionId, checkIndex, {
status: 'completed',
loopIndex,
sourceIteration,
taskId,
processId,
startedAt: session?.finalChecks?.find(c => c.checkIndex === checkIndex)?.startedAt ?? nowIso,
completedAt: nowIso,
...baseCheckRecord,
hasGaps: true,
gapCount: parsed.gaps.length,
gapLoopStarted: false,
Expand Down Expand Up @@ -225,30 +200,21 @@ export async function orchestrateFinalCheck(input: OrchestrateFinalCheckInput):
logger.warn(LogCategory.AI, `[Ralph/FinalCheck] enqueue gap-fix failed for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
await safeUpsertRecord(store, workspaceId, sessionId, checkIndex, {
status: 'completed',
loopIndex,
sourceIteration,
taskId,
processId,
startedAt: session?.finalChecks?.find(c => c.checkIndex === checkIndex)?.startedAt ?? nowIso,
completedAt: nowIso,
...baseCheckRecord,
hasGaps: true,
gapCount: parsed.gaps.length,
gapLoopStarted: false,
goalSynthesized,
}, logger);
broadcastSessionComplete({ workspaceId, sessionId, processId, totalIterations: sourceIteration, reason: 'final-check-gap-enqueue-failed' });
return;
}

logger.debug(LogCategory.AI, `[Ralph/FinalCheck] Gap-fix loop ${newLoopIndex} enqueued as task ${newTaskId} for session ${sessionId}.`);

await safeUpsertRecord(store, workspaceId, sessionId, checkIndex, {
status: 'completed',
loopIndex,
sourceIteration,
taskId,
processId,
startedAt: session?.finalChecks?.find(c => c.checkIndex === checkIndex)?.startedAt ?? nowIso,
completedAt: nowIso,
...baseCheckRecord,
hasGaps: true,
gapCount: parsed.gaps.length,
gapLoopStarted: true,
Expand All @@ -261,6 +227,14 @@ export async function orchestrateFinalCheck(input: OrchestrateFinalCheckInput):
// Private helpers
// ============================================================================

function buildBaseCheckRecord(
fields: Pick<RalphFinalCheckRecord, 'loopIndex' | 'sourceIteration' | 'taskId' | 'processId'>,
startedAt: string,
nowIso: string,
): Pick<RalphFinalCheckRecord, 'loopIndex' | 'sourceIteration' | 'taskId' | 'processId' | 'startedAt' | 'completedAt'> {
return { ...fields, startedAt, completedAt: nowIso };
}

async function safeUpsertRecord(
store: RalphSessionStore,
workspaceId: string,
Expand Down
Loading
Loading