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
28 changes: 27 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Destructive commands (need permission): `zeroshot kill`, `zeroshot clear`, `zero
| Gateway tools/policy | `src/agent-cli-provider/gateway-tools.ts` |
| Provider detection | `lib/provider-detection.js` |
| Provider capabilities | `src/providers/capabilities.js` |
| Provider session reuse | `src/agent/provider-session.js` |
| Start-cluster helper | `lib/start-cluster.js` |
| Legacy worker facade | `lib/cluster-worker/` |
| Legacy worker executable | `bin/zeroshot-cluster-worker.js` |
Expand Down Expand Up @@ -331,13 +332,38 @@ Restart persistence: orchestrator publishes `AGENT_RESTART_ATTEMPT` to the ledge
Provider task ownership: task watchers persist an owned termination boundary with each active task.
POSIX providers run in a dedicated process group; Windows providers use the exact root PID with
`taskkill /T`. Recovery must terminate that recorded boundary before retrying work.
Provider continuation is agent- and generation-owned and becomes durable only after logical output
validation and the `onComplete` hook succeed. A requested resume is successful only when the
watcher captures that exact same nonempty provider session ID; absent or forked identity fails the
attempt before hooks and forces the retry to rebuild full context. Watchers track every unique
session ID observed in a task; once two IDs differ, the persisted capture is permanently ambiguous
even if a later event repeats the requested ID. Persist SQLite rowid high-water and applied-guidance
cursors as canonical decimal strings, bind them to SQLite as `BigInt`, and never coerce them through
JavaScript `Number`. Persist those cursors and a bounded SHA-256 selected-prompt identity with the
observed provider session; never persist the selected prompt text. Restored
continuations fail closed unless the final durable `TASK_COMPLETED` boundary and all provenance
match. Full and continuation source/guidance reads are bounded through the captured high-water;
continuations query strictly after their prior sequence and de-duplicate the exact triggering
message by ledger ID. Timestamps are display/filter metadata, not continuation cursors: concurrent
writers can share one millisecond. If the installed CLI cannot resume, rebuild full context or fail
before launch—never send a continuation delta to a fresh provider session.

Provider session reuse is explicit-ID and agent-owned. Watcher-observed IDs are distinct from
requested resume IDs. Commit continuation only after logical/structured success and bind it to the
completed task, agent, generation, provider, cwd, and worktree. A resumed turn sends only new
trigger/guidance context; it never replays static prompts or ISSUE_OPENED/PLAN_READY packs already in
the provider session. Persist continuation in that agent's `agentStates` entry, never in native
`ClusterLedger`, never select a cwd-wide "latest" session, and never share across agents. Durable
restore fails closed unless the last lifecycle boundary is the exact matching `TASK_COMPLETED`;
live, failed, retry/backoff, provider-switch, unsupported, Docker, and workspace-drift states start
fresh.

### Guidance Messaging

- Topics: `USER_GUIDANCE_CLUSTER`, `USER_GUIDANCE_AGENT` (see `src/guidance-topics.js`).
- Mailbox helper: `ledger.queryGuidanceMailbox()` with `messageBus.queryGuidanceMailbox()` passthrough.
- Live injection: `Orchestrator.sendGuidanceToAgent()` uses `agent.injectInput()` to attempt PTY stdin; always persists `USER_GUIDANCE_AGENT` with `metadata.delivery` (`status: injected|unsupported`, `method: pty`, `taskId`, `reason`).
- Safe-point queue fallback: `AgentWrapper._buildContext()` pulls queued guidance via `collectQueuedGuidance()` and injects a delimited block in `agent-context-builder` between Instructions and Output Schema. Cursor: `agent.lastGuidanceAppliedAt`.
- Safe-point queue fallback: `AgentWrapper._buildContext()` pulls queued guidance via `collectQueuedGuidance()` and injects a delimited block in `agent-context-builder` between Instructions and Output Schema. Durable sequence: `agent.lastGuidanceAppliedId`.

### Agent Configuration (Minimal)

Expand Down
1 change: 1 addition & 0 deletions cli/commands/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ function buildTaskSummary(task, details) {
error: task.error || null,
cwd: task.cwd || null,
sessionId: task.sessionId || null,
requestedResumeSessionId: task.requestedResumeSessionId || null,
attachable: Boolean(task.attachable),
socketPath: details.socketPath.path,
socketPathExists: details.socketPath.exists,
Expand Down
2 changes: 1 addition & 1 deletion cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2622,7 +2622,7 @@
Force provider flags: -G (GitHub), -L (GitLab), -J (Jira), -D (DevOps), -N (Linear)
`
)
.action(async (inputArg, options) => {

Check warning on line 2625 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Async arrow function has a complexity of 31. Maximum allowed is 20

Check warning on line 2625 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Refactor this function to reduce its Cognitive Complexity from 53 to the 15 allowed

Check warning on line 2625 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Async arrow function has too many lines (158). Maximum allowed is 150
try {
// Normalize options (--ship → --pr → --worktree flags)
normalizeRunOptions(options);
Expand Down Expand Up @@ -2837,7 +2837,7 @@
.option('--model <model>', 'Model id override for the provider')
.option('--model-level <level>', 'Model level override (level1, level2, level3)')
.option('--reasoning-effort <effort>', 'Reasoning effort (low, medium, high, xhigh, max)')
.option('-r, --resume <sessionId>', 'Resume a specific Claude session (claude only)')
.option('-r, --resume <sessionId>', 'Resume a specific provider session (Claude or Codex)')
.option('-c, --continue', 'Continue the most recent Claude session (claude only)')
.option(
'-o, --output-format <format>',
Expand Down Expand Up @@ -3115,7 +3115,7 @@
.command('kill-all')
.description('Kill all running tasks and clusters')
.option('-y, --yes', 'Skip confirmation')
.action(async (options) => {

Check warning on line 3118 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Refactor this function to reduce its Cognitive Complexity from 26 to the 15 allowed
try {
// Get counts first
const orchestrator = await getOrchestrator();
Expand Down Expand Up @@ -3339,7 +3339,7 @@
.command('resume <id> [prompt]')
.description('Resume a failed task or cluster')
.option('-d, --detach', 'Resume in background (daemon mode)')
.action(async (id, prompt, options) => {

Check warning on line 3342 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Async arrow function has a complexity of 30. Maximum allowed is 20

Check warning on line 3342 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Refactor this function to reduce its Cognitive Complexity from 38 to the 15 allowed

Check warning on line 3342 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Async arrow function has too many lines (181). Maximum allowed is 150
let orchestrator = null;
let keepOrchestratorOpen = false;
try {
Expand Down Expand Up @@ -4677,7 +4677,7 @@
}
});

function outputAgent(agent, options) {

Check warning on line 4680 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed
if (options.json) {
console.log(JSON.stringify(agent, null, 2));
return;
Expand Down Expand Up @@ -4845,7 +4845,7 @@
}

// Format tool result for display
function formatToolResult(content, isError, toolName, toolInput) {

Check warning on line 4848 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed
if (!content) return isError ? 'error' : 'done';

// For errors, show full message
Expand Down Expand Up @@ -5525,7 +5525,7 @@

// Accumulate text and print complete lines only
// Word wrap long lines, aligning continuation with message column
function accumulateText(prefix, sender, text) {

Check warning on line 5528 in cli/index.js

View workflow job for this annotation

GitHub Actions / check

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed
if (!text) return;
const buf = getLineBuffer(sender);

Expand Down
21 changes: 18 additions & 3 deletions src/agent-cli-provider/adapters/claude.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { stringifyJson } from '../json';
import { getString, isRecord, stringifyJson, tryParseJson } from '../json';
import {
type BuildProviderCommandOptions,
type ClaudeCliFeatures,
Expand Down Expand Up @@ -66,6 +66,7 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
supportsVerbose: unknown ? true : /--verbose/.test(help),
supportsModel: unknown ? true : /--model/.test(help),
supportsEffort: unknown ? true : /--effort/.test(help),
supportsResume: unknown ? true : /--resume/.test(help),
unknown,
};
}
Expand Down Expand Up @@ -115,15 +116,28 @@ function addAutoApproveArgs(args: string[], options: BuildProviderCommandOptions
}

function addSessionArgs(args: string[], options: BuildProviderCommandOptions): void {
if (options.resumeSessionId) {
const features = optionFeatures(options);
if ((options.resumeSessionId || options.continueSession) && features.supportsResume === false) {
throw new Error(
'Claude CLI cannot safely run continuation context because this installation lacks --resume.'
);
}
if (options.resumeSessionId && features.supportsResume !== false) {
args.push('--resume', options.resumeSessionId);
return;
}
if (options.continueSession) {
if (options.continueSession && features.supportsResume !== false) {
args.push('--continue');
}
}

function extractSessionId(line: string): string | null {
const event = tryParseJson(line.trim());
if (!isRecord(event)) return null;
const sessionId = getString(event, 'session_id');
return sessionId?.trim() || null;
}

function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
const features = optionFeatures(options);
const warnings: WarningMetadata[] = [];
Expand Down Expand Up @@ -228,6 +242,7 @@ export const claudeAdapter: ProviderAdapter = {
defaultMinLevel: 'level1',
detectCliFeatures,
buildCommand,
extractSessionId,
parseEvent: parseClaudeEvent,
createParserState: () => createParserState('claude'),
resolveModelSpec,
Expand Down
50 changes: 47 additions & 3 deletions src/agent-cli-provider/adapters/codex.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getString, isRecord, tryParseJson } from '../json';
import { appendJsonSchemaPrompt, writeStrictOutputSchemaFile } from '../schema';
import {
type BuildProviderCommandOptions,
Expand All @@ -18,7 +19,6 @@ import {
createParserState,
optionFeatures,
resolveModelSpecWithConfig,
unsupportedSessionControlWarnings,
validateModelIdFromCatalog,
warning,
} from './common';
Expand Down Expand Up @@ -58,10 +58,18 @@ function detectCliFeatures(helpText?: string | null): CodexCliFeatures {
supportsConfigOverride: supports(help, /--config\b/),
supportsModel: supports(help, /\s-m\b/) || supports(help, /--model\b/),
supportsSkipGitRepoCheck: supports(help, /--skip-git-repo-check\b/),
supportsResume: supports(help, /\bresume\b/),
unknown,
};
}

function extractSessionId(line: string): string | null {
const event = tryParseJson(line.trim());
if (!isRecord(event) || getString(event, 'type') !== 'thread.started') return null;
const sessionId = getString(event, 'thread_id');
return sessionId?.trim() || null;
}

function addOutputArgs(args: string[], options: BuildProviderCommandOptions): void {
const features = optionFeatures(options);
if (
Expand Down Expand Up @@ -121,7 +129,25 @@ function applySchemaArgs(

function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[] {
const features = optionFeatures(options);
const warnings: WarningMetadata[] = unsupportedSessionControlWarnings('codex', options);
const warnings: WarningMetadata[] = [];
if (options.continueSession) {
warnings.push(
warning(
'codex',
'unsupported-session-control',
'Codex requires an explicit session ID; ignoring continueSession.'
)
);
}
if (options.resumeSessionId && features.supportsResume === false) {
warnings.push(
warning(
'codex',
'codex-session-resume-unsupported',
'Codex CLI does not support exec resume; starting a fresh session.'
)
);
}
if (options.autoApprove && features.supportsAutoApprove === false) {
warnings.push(
warning(
Expand Down Expand Up @@ -153,16 +179,33 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
}

function buildCommand(context: string, options: BuildProviderCommandOptions = {}): CommandSpec {
if (options.resumeSessionId && optionFeatures(options).supportsResume === false) {
throw new Error(
'Codex CLI cannot safely run continuation context because this installation lacks exec resume.'
);
}
const args: string[] = ['exec'];
const cleanup: string[] = [];
const resumeSessionId =
options.resumeSessionId && optionFeatures(options).supportsResume !== false
? options.resumeSessionId
: null;
if (resumeSessionId) {
args.push('resume');
}

addOutputArgs(args, options);
addModelArgs(args, options);
addCwdArgs(args, options);
if (!resumeSessionId) {
addCwdArgs(args, options);
}
addAutoApproveArgs(args, options);
addSkipGitArgs(args, options);
const finalContext = applySchemaArgs(args, cleanup, context, options);

if (resumeSessionId) {
args.push(resumeSessionId);
}
args.push(finalContext);

return commandSpec({
Expand Down Expand Up @@ -216,6 +259,7 @@ export const codexAdapter: ProviderAdapter = {
defaultMinLevel: 'level1',
detectCliFeatures,
buildCommand,
extractSessionId,
parseEvent: parseCodexEvent,
createParserState: () => createParserState('codex'),
resolveModelSpec,
Expand Down
2 changes: 1 addition & 1 deletion src/agent-cli-provider/adapters/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function unsupportedSessionControlWarnings(
warning(
provider,
'unsupported-session-control',
'resume/continue is only supported for Claude CLI; ignoring.'
`Provider ${provider} does not support resume/continue session control; ignoring.`
),
];
}
Expand Down
10 changes: 10 additions & 0 deletions src/agent-cli-provider/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ export function parseProviderChunk(
return events;
}

export function extractProviderSessionId(
providerName: KnownProviderName | string,
line: string
): string | null {
const adapter = getProviderAdapter(providerName || 'claude');
const content = stripTimestampPrefix(line);
if (!content || !adapter.extractSessionId) return null;
return adapter.extractSessionId(content);
}

export function resolveModelSpec(
providerName: KnownProviderName | string,
level: ModelLevel,
Expand Down
1 change: 1 addition & 0 deletions src/agent-cli-provider/contract-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const CLI_FEATURE_FIELDS = [
'supportsNoAskUser',
'supportsAddDir',
'supportsMcpConfig',
'supportsResume',
'supportsBundledRunner',
'supportsAcpStdio',
'supportsPromptImages',
Expand Down
1 change: 1 addition & 0 deletions src/agent-cli-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
classifyProviderError,
detectProviderFatalError,
detectProviderStreamingModeError,
extractProviderSessionId,
getProviderAdapter,
listProviderAdapters,
parseProviderChunk,
Expand Down
14 changes: 13 additions & 1 deletion src/agent-cli-provider/provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
readonly streamJson: ProviderCapabilityState;
readonly thinkingMode: ProviderCapabilityState;
readonly reasoningEffort: ProviderCapabilityState;
readonly sessionResume: ProviderCapabilityState;
}

interface FixedProviderCommandSpec {
Expand Down Expand Up @@ -93,13 +94,22 @@
}

const STANDARD_CAPABILITIES: Readonly<
Pick<ProviderCapabilities, 'dockerIsolation' | 'worktreeIsolation' | 'mcpServers' | 'streamJson' | 'thinkingMode'>
Pick<
ProviderCapabilities,
| 'dockerIsolation'
| 'worktreeIsolation'
| 'mcpServers'
| 'streamJson'
| 'thinkingMode'
| 'sessionResume'
>
> = {
dockerIsolation: true,
worktreeIsolation: true,
mcpServers: true,
streamJson: true,
thinkingMode: true,
sessionResume: false,
};

const CLAUDE_DOCKER_ENV_PASSTHROUGH = [
Expand Down Expand Up @@ -161,6 +171,7 @@
...STANDARD_CAPABILITIES,
jsonSchema: true,
reasoningEffort: true,
sessionResume: true,
},
docs: {
label: 'Claude',
Expand Down Expand Up @@ -197,6 +208,7 @@
...STANDARD_CAPABILITIES,
jsonSchema: true,
reasoningEffort: true,
sessionResume: true,
},
docs: {
label: 'Codex',
Expand Down Expand Up @@ -519,5 +531,5 @@
name: string,
capability: keyof ProviderCapabilities
): boolean {
return getProviderRegistryEntry(name).capabilities[capability] === true;

Check warning on line 534 in src/agent-cli-provider/provider-registry.ts

View workflow job for this annotation

GitHub Actions / check

File has too many lines (502). Maximum allowed is 500
}
4 changes: 4 additions & 0 deletions src/agent-cli-provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface ClaudeCliFeatures extends BaseCliFeatures {
readonly supportsVerbose: boolean;
readonly supportsModel: boolean;
readonly supportsEffort: boolean;
readonly supportsResume: boolean;
}

export interface CodexCliFeatures extends BaseCliFeatures {
Expand All @@ -112,6 +113,7 @@ export interface CodexCliFeatures extends BaseCliFeatures {
readonly supportsConfigOverride: boolean;
readonly supportsModel: boolean;
readonly supportsSkipGitRepoCheck: boolean;
readonly supportsResume: boolean;
}

export interface GeminiCliFeatures extends BaseCliFeatures {
Expand Down Expand Up @@ -212,6 +214,7 @@ export interface CliFeatureOverrides {
readonly supportsNoAskUser?: boolean;
readonly supportsAddDir?: boolean;
readonly supportsMcpConfig?: boolean;
readonly supportsResume?: boolean;
readonly supportsBundledRunner?: boolean;
readonly supportsAcpStdio?: boolean;
readonly supportsPromptImages?: boolean;
Expand Down Expand Up @@ -368,6 +371,7 @@ export interface ProviderAdapter {
readonly defaultMinLevel: ModelLevel;
detectCliFeatures(helpText?: string | null): ProviderCliFeatures;
buildCommand(context: string, options?: BuildProviderCommandOptions): CommandSpec;
extractSessionId?(line: string): string | null;
parseEvent(line: string, state: ProviderParserState): ProviderParseResult;
createParserState(): ProviderParserState;
resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec;
Expand Down
Loading
Loading