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
6 changes: 5 additions & 1 deletion PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ and unknown leases while digesting only proven released history. Dated UI drafts
asset measurements and test reports remain in [the archive](docs/history/2026-11-02-collaboration-implementation.md).

### Task-scoped SDK context and resident Chief memory — 2026-11-01
Owner requires a resident Chief with durable Project memory and one logical Session per Task/role. Codex/Claude own their supported conversation persistence and automatic compaction; FlyBot does not inject a second model-history compactor. SDK role rework reuses a confirmed released Task-role cwd/thread under the same principal/resource/profile/model/persona binding, while preserving separate immutable Attempt/claim/log/test/Review evidence. Changed or incompatible bindings expose an explicit context-resume problem and retain history; they cannot silently start a fresh conversation. Chief memory uses the original own-Bot MEMORY.md/topics/journal, never global team or private-user memory. Context admission keeps current bounded facts and hash-bound full source archives for scoped retrieval. Backend changes are foreground work; frontend Task40/PR48 resumes through FlyBot after validation.
The original implementation checkpoint is [preserved verbatim](docs/history/2026-11-04-context-implementation-checkpoint.md). Current contracts and acceptance follow.

Keep Chief durable memory and one logical Session per Task/role. SDKs retain their
supported persistence/compaction; FlyBot keeps bounded facts and exact binding/exit
Expand Down Expand Up @@ -314,3 +314,7 @@ Task execution pins its associated repository independently of Project defaults;
#2334 follow-up (updated issue): shell 127 / missing test runners are environment
blockers. testsStarted records command invocation; stage distinguishes runner startup
from preparation and tests. Nested Node test markers do not enter host verification.

#2338 preflights input receipts before host side effects and returns correctable
errors to the original role/Chief Session. Frozen proposals, bounded tool-free
correction and durable receipts preserve work; stale/revoked input stays denied.
5 changes: 5 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,8 @@ deferred multi-repository automation is not reactivated by this recovery work.
Updated #2334 acceptance adds actual npm missing-vitest/127 classification and
clean/prepare/same-candidate retest coverage; it remains a source/test follow-up,
not live npm ci provisioning or production Vitest acceptance.

#2338: bounded input receipt correction in the original Task-role/Chief context,
with all-plan preflight, immutable business evidence and explicit incomplete
recovery. Tests distinguish deterministic role transport and actual SDK execution
with a local provider fixture from paid-model/live project acceptance.
1 change: 1 addition & 0 deletions changelog.d/2338.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Return correctable input receipt errors to the original role or Chief Session before host completion, with bounded tool-free correction and immutable business evidence.
11 changes: 11 additions & 0 deletions core/src/host/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,17 @@ export function install(rt) {
if (result?.ok === false) throw new Error(result.code ?? 'Pool dispatch failed')
if (taskInbox?.inputs.length && !rt.state.projectHarnessWorkers?.[workerId]) await rt.projectInputs.transport(task.id, claim, taskInbox.batchId, { kind: 'adapter-written', sha256: createHash('sha256').update(JSON.stringify(taskInbox)).digest('hex') })
},
async correct(input) {
const sdk = await rt.projectHarness?.correctInput(input)
if (sdk) return sdk
if (typeof rt.ctx.tools?.guard !== 'function') throw new Error('Native receipt correction requires the tool guard')
const sessionId = rt.chatOf(input.workerId), agent = sessionId && await rt.resolveAgent(sessionId)
if (!agent || agent.session?.id !== sessionId || typeof agent.whenIdle !== 'function') throw new Error('Original native receipt Session is unavailable')
await agent.whenIdle()
const c = input.task.collaboration
if (rt.chatOf(input.workerId) !== sessionId || c.claim?.id !== input.claim.id || c.generation !== input.claim.generation || c.claim.inputCorrection?.round !== input.round || c.claim.inputCorrection?.status !== 'pending') throw new Error('Native receipt correction was superseded')
return lifecycle.dispatch({ botId: input.workerId, taskId: input.task.id, brief: input.prompt, inputs: {}, outputs: [{ name: 'handoff', required: true }] })
},
async release(id) {
await lifecycle.release(id)
return rt.projectDelivery.exitConfirmed(id)
Expand Down
15 changes: 13 additions & 2 deletions core/src/host/project-chief-control.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions core/src/host/project-development-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export function taskView(state, task, { activity = {}, roleCatalog, isIntentComm
const nativeCandidate = ownEntry(state.projectNativeWorkers ?? {}, claim?.workerId)
const nativeWorker = sameTask(nativeCandidate, task) ? nativeCandidate : null
const executionView = executionMetadataView(state, sdk ?? nativeWorker)
const receiptCorrection = claim?.inputCorrection
if (executionView && receiptCorrection) executionView.inputCorrection = { version: receiptCorrection.version, round: receiptCorrection.round, limit: receiptCorrection.limit, status: receiptCorrection.status, proposalSha256: receiptCorrection.proposal?.sha256 ?? null, feedback: receiptCorrection.feedback.map(({ kind, field, correctable, sideEffects, businessEffects }) => ({ kind, field, correctable, sideEffects, businessEffects })) }
const nativeActivated = nativeWorker && isIntentCommitted(nativeWorker.activationEffect)
const native = ownEntry(state.bots ?? {}, claim?.workerId)
const nativeActive = native?.projectId === task.projectId && (!attempt || native.attemptId === attempt.id) && activity[claim?.workerId]
Expand All @@ -63,6 +65,7 @@ export function taskView(state, task, { activity = {}, roleCatalog, isIntentComm
else if (task.status === 'cancelled' || c?.status === 'cancelled' || attempt?.status === 'cancelled') phase = 'cancelled'
else if (attempt?.cancelRequested || c?.status === 'stopping') { phase = 'stopping'; waitingFor = 'worker-exit' }
else if (nativeWorker?.status === 'stopping' || nativeWorker?.status === 'stop-unconfirmed') { phase = nativeWorker.status === 'stopping' ? 'stopping' : 'blocked'; waitingFor = 'worker-exit'; reason = c?.block?.reason ?? null }
else if (receiptCorrection?.status === 'pending') { phase = 'awaiting-input'; waitingFor = 'role-correction'; reason = 'Original role is correcting input receipts; business proposal is preserved' }
else if (task.chiefDecisionIssue?.status === 'pending') { phase = 'awaiting-chief'; waitingFor = 'chief-correction'; reason = task.chiefDecisionIssue.feedback.message }
else if (task.chiefDecisionIssue?.status === 'exhausted') { phase = 'blocked'; waitingFor = 'chief-recovery'; reason = 'Chief judgement incomplete; correction budget exhausted' }
else if (task.chiefQuestion) { phase = 'awaiting-owner'; waitingFor = task.chiefQuestion.recipient?.kind === 'personal-agent' ? 'personal-agent' : 'owner'; reason = task.chiefQuestion.reason }
Expand Down
11 changes: 6 additions & 5 deletions core/src/host/project-harness-turn.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url'
import { validateCodexConfiguration } from './project-codex-config.js'
// One real SDK turn. The host owns Workflow completion and acceptance, never this driver.
export async function runHarnessTurn(request, { emit, permission, signal, validateCodex = validateCodexConfiguration, loadCodex = () => import('@openai/codex-sdk'), loadClaude = () => import('@anthropic-ai/claude-agent-sdk') }) {
const toolsDisabled = request.noTools || request.protocolCorrection === true
let text = '', completed = false, sessionId = request.sessionId ?? null, usage = null
if (request.harness === 'codex') {
const toolOutputTokens = request.toolOutputTokens ?? 2000
Expand All @@ -10,7 +11,7 @@ export async function runHarnessTurn(request, { emit, permission, signal, valida
const denyScript = fileURLToPath(new URL('./project-harness-deny-tool.mjs', import.meta.url))
const quote = value => `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('$', '\\$').replaceAll('`', '\\`')}"`
const connectorPolicy = { remote_plugin: false, skill_mcp_dependency_install: false }
const chiefConfig = request.noTools ? { features: { ...connectorPolicy, shell_tool: false }, tools: { view_image: false }, apps: { _default: { enabled: false } },
const chiefConfig = toolsDisabled ? { features: { ...connectorPolicy, shell_tool: false }, tools: { view_image: false }, apps: { _default: { enabled: false } },
hooks: { PreToolUse: [{ matcher: '.*', hooks: [{ type: 'command', command: `node ${quote(denyScript)}` }] }] } } : { features: connectorPolicy }
// Raw network denial also denies Unix pipe lifecycle syscalls in CLI 0.166,
// breaking Node child_process despite a successful child exit. The limited
Expand All @@ -22,7 +23,7 @@ export async function runHarnessTurn(request, { emit, permission, signal, valida
const inlineToml = value => value && typeof value === 'object' ? '{' + Object.entries(value).map(([key, child]) => `${JSON.stringify(key)}=${inlineToml(child)}`).join(',') + '}' : JSON.stringify(value)
const rawPolicy = request.noTools ? [] : ['permissions.flybot-role=' + inlineToml(processPolicy.permissions['flybot-role'])]
const configPolicy = request.noTools ? {} : { ...processPolicy, permissions: { 'flybot-role': { extends: processPolicy.permissions['flybot-role'].extends, network: processPolicy.permissions['flybot-role'].network } } }
const clientOptions = { env: request.env, configOverrides: ['mcp_servers={}', 'plugins={}', 'apps={_default={enabled=false}}', ...rawPolicy], ...(request.baseUrl ? { baseUrl: request.baseUrl } : {}), config: { ...(request.config ?? {}), ...chiefConfig, ...configPolicy, tool_output_token_limit: toolOutputTokens } }
const clientOptions = { env: request.env, configOverrides: ['mcp_servers={}', 'plugins={}', 'apps={_default={enabled=false}}', ...rawPolicy], ...(request.baseUrl ? { baseUrl: request.baseUrl } : {}), config: { ...(request.config ?? {}), ...chiefConfig, ...configPolicy, features: { ...chiefConfig.features, ...configPolicy.features, ...(toolsDisabled ? { shell_tool: false } : {}) }, tool_output_token_limit: toolOutputTokens } }
const validated = await validateCodex({ ...request, signal }, clientOptions)
if (validated.capability) await emit({ type: 'configuration-checked', harness: 'codex', capability: validated.capability })
const client = new Codex({ ...clientOptions, codexPathOverride: validated.codexPathOverride, env: validated.env })
Expand All @@ -48,10 +49,10 @@ export async function runHarnessTurn(request, { emit, permission, signal, valida
settingSources: [], mcpServers: {}, persistSession: true,
...(sessionId ? { resume: sessionId } : {}), maxTurns: request.probe ? 1 : 30, maxBudgetUsd: request.maxBudgetUsd ?? 1,
permissionMode: 'default',
tools: request.noTools ? [] : request.readOnly ? ['Read', 'Glob', 'Grep', 'Bash'] : ['Read', 'Glob', 'Grep', 'Bash', 'Edit', 'Write'],
hooks: request.noTools ? { PreToolUse: [{ hooks: [async () => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'Use FlyBot scoped action proposals only' } })] }] } : {},
tools: toolsDisabled ? [] : request.readOnly ? ['Read', 'Glob', 'Grep', 'Bash'] : ['Read', 'Glob', 'Grep', 'Bash', 'Edit', 'Write'],
hooks: toolsDisabled ? { PreToolUse: [{ hooks: [async () => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'Use FlyBot scoped action proposals only' } })] }] } : {},
canUseTool: async (name, input, options) => {
if (request.noTools) return { behavior: 'deny', message: 'Generic Chief tools are disabled' }
if (toolsDisabled) return { behavior: 'deny', message: 'Generic Chief tools are disabled' }
const answer = await permission({ name, input, requestId: options.requestId ?? options.toolUseID })
return answer.allow ? { behavior: 'allow', updatedInput: input } : { behavior: 'deny', message: 'FlyBot operator refused this action' }
},
Expand Down
14 changes: 10 additions & 4 deletions core/src/host/project-harness-worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,26 @@ const input = createInterface({ input: process.stdin, crlfDelay: Infinity })
const write = value => process.stdout.write(JSON.stringify(value) + '\n')
const answers = new Map()
let controller, sessionId, settings, busy = false, shuttingDown = false
const completedTurns = new Map()
const permission = request => new Promise(resolve => {
const id = randomUUID(); answers.set(id, resolve); write({ type: 'permission', id, request })
})
async function turn(message) {
if (message.turnId && completedTurns.has(message.turnId)) { write(completedTurns.get(message.turnId)); return }
if (busy) { write({ type: 'failure', reason: 'SDK Session is already answering' }); return }
busy = true; controller = new AbortController()
try {
settings ??= message.settings
sessionId ??= settings.sessionId
const result = await runHarnessTurn({ ...settings, sessionId, prompt: message.prompt }, { emit: write, permission, signal: controller.signal })
const emit = value => write({ ...value, ...(message.turnId ? { turnId: message.turnId } : {}) })
const result = await runHarnessTurn({ ...settings, sessionId, protocolCorrection: message.protocolCorrection === true, prompt: message.prompt }, { emit, permission, signal: controller.signal })
sessionId = result.sessionId
write({ type: 'completed', ...result })
const response = { type: 'completed', ...result, ...(message.turnId ? { turnId: message.turnId } : {}) }
if (message.turnId) { completedTurns.set(message.turnId, response); if (completedTurns.size > 16) completedTurns.delete(completedTurns.keys().next().value) }
busy = false
write(response)
if (settings.probe) void shutdown()
} catch (error) { write({ type: 'failure', reason: ['bot/harness-capability', 'bot/context-resume'].includes(error?.code) ? error.message : 'SDK execution failed; inspect the redacted event evidence and resource environment', ...(error?.code === 'bot/context-resume' ? { contextFailure: { code: error.code, kind: 'backend-session-mismatch', contextPreserved: true } } : {}), ...(error?.code === 'bot/harness-capability' ? { capabilityFailure: { code: error.code, kind: error.capability, stage: 'configuration', modelInvoked: false } } : {}) }) }
} catch (error) { write({ type: 'failure', ...(message.turnId ? { turnId: message.turnId } : {}), reason: ['bot/harness-capability', 'bot/context-resume'].includes(error?.code) ? error.message : 'SDK execution failed; inspect the redacted event evidence and resource environment', ...(error?.code === 'bot/context-resume' ? { contextFailure: { code: error.code, kind: 'backend-session-mismatch', contextPreserved: true } } : {}), ...(error?.code === 'bot/harness-capability' ? { capabilityFailure: { code: error.code, kind: error.capability, stage: 'configuration', modelInvoked: false } } : {}) }) }
finally { busy = false }
}
input.on('line', line => {
Expand Down Expand Up @@ -55,4 +61,4 @@ function sdkVersion(name) {
} catch { /* Missing SDK is an explicit failed handshake, not a simulated version. */ }
return null
}
write({ type: 'ready', pid: process.pid, sdk: { codex: sdkVersion('@openai/codex-sdk'), 'claude-agent': sdkVersion('@anthropic-ai/claude-agent-sdk') } })
write({ type: 'ready', receiptCorrection: 'input-correction/v1', pid: process.pid, sdk: { codex: sdkVersion('@openai/codex-sdk'), 'claude-agent': sdkVersion('@anthropic-ai/claude-agent-sdk') } })
Loading
Loading