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
8 changes: 8 additions & 0 deletions LESSONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Dated, short lessons from real FlyBot work. Agents read this **before coding**.

## Open lessons

### 2026-11-04 — Releasing a slot requires re-admission and lock ordering (Seen 1×)

A suspended Attempt still has a Run; filter both using the same trusted exit facts.
Resume through the original quota queue before claiming. Never await queue drain
inside the pool lock: new-task drain can itself need that lock. Save acknowledgement
must precede dispatch; input receipt alone is neither resumption nor exit proof.


### 2026-11-04 — Protocol errors are feedback, not business decisions (Seen 1×)

A missing routing field stalled an otherwise configured pool. Validate the whole
Expand Down
4 changes: 3 additions & 1 deletion PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ Current stage and priorities: [ROADMAP.md](ROADMAP.md). How decisions are made:
preserves drafts and admits explicit inspect/integrate inputs through existing
Effects. Chief selects only approved repository sources. #2327 adds bounded, durable Chief protocol correction in the resident Session;
exhaustion retains incomplete judgement without automatic escalation. Follow-ups:
#2328 idle input-wait slots, #2330 inspection recovery. See
#2328 adds confirmed-idle wait suspension and FIFO continuation re-admission
through the original queue; exit-unknown capacity remains held. Remaining: #2330
inspection recovery. See
[delivery contract](docs/subsystems/project-delivery.md). Live Task mutation and
deployment remain separate from source/test delivery.

Expand Down
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ Implement and merge #2325 → #2327 → #2328 → #2330 sequentially into
workspace materialization and explicit source admission; Chief owns recovery
choices, the host preserves authority, evidence and execution-exit gates.
#2327 implements same-Session correction with structured feedback and a configured
budget; no implicit role routing or human escalation. Remaining: #2328 and #2330.
budget; no implicit role routing or human escalation. #2328 adds exit-proven input waits and shared new/resume admission.
Remaining: #2330 scheduled inspection restart recovery.

## Agent connection authorization — 2026-11-03

Expand Down
1 change: 1 addition & 0 deletions changelog.d/2328.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Confirmed-idle input waits release project execution capacity while retaining Task, Attempt and Session state. Continuations share the original FIFO queue and regain project/resource capacity before role-pool execution; exit-unknown work stays held.
1 change: 1 addition & 0 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.

1 change: 1 addition & 0 deletions core/src/host/project-delivery.js
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,7 @@ export function createProjectDelivery(rt, { publisher, releaseProvider } = {}) {
if (['running', 'waiting-acceptance'].includes(previous.status)) previous.status = 'returned'
previous.returned = { at: Date.now(), by: claim.botId, reason }
const next = { id, protocol: previous.protocol, projectId: previous.projectId, taskId: task.id, requestKey: hash(`${task.projectId}\n${task.id}\n${claim.id}`), workflowVersion: previous.protocol === 'project-handoff/v1' ? 'project-delivery/pools-v4' : previous.workflowVersion, status: 'running', initiatedBy: previous.initiatedBy, executionOwnerId: previous.executionOwnerId, baseSha: previous.requirement.repository && !previous.candidateSha ? await git(settingsOf(projectOf(task.projectId, previous)).path, ['rev-parse', 'HEAD']) : previous.baseSha, seedSha: previous.candidateSha ?? previous.seedSha, ...(publicationBinding ? { publicationBinding } : {}), ...(previous.integrationInputs ? { integrationInputs: structuredClone(previous.integrationInputs), integrationAdmissions: structuredClone(previous.integrationAdmissions ?? []) } : {}), repositoryBinding: previous.repositoryBinding, requirement: requirementsOf(task, previous.requirement.repository), feedback: { previousAttemptId: previous.id, kind: 'bot-return', reason, candidateSha: previous.candidateSha }, agentDefinitions: rt.projectAgents.deliveryDefinitions(task.projectId), roleSessions: {}, rolePolicies: {}, resourceLeases: {}, workspaces: {}, evidence: {}, createdAt: Date.now(), updatedAt: Date.now(), runId: `pool-run-${randomUUID()}` }
if (previous.capacityWait && previous.capacityWait.status !== 'admitted') next.capacityWait = { ...structuredClone(previous.capacityWait), attemptId: id, previousAttemptId: previous.id }
previous.supersededBy = id
setEntry(attempts(), id, next); task.deliveryAttemptId = id
rt.bindProject({ kind: 'run', id: previous.runId, projectId: task.projectId, taskId: task.id, attemptId: previous.id, status: 'failed' })
Expand Down
26 changes: 26 additions & 0 deletions core/src/host/project-execution-capacity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Waiting is a Chief/Task decision. Capacity suspension additionally requires host exit facts.
import { confirmedWorkerExit } from './project-task-sessions.js'
import { nativeExitConfirmed } from './project-native-sessions.js'
export function attemptIdle(state, attemptId) {
const attempt = state.projectAttempts?.[attemptId], task = state.tasks?.[attempt?.taskId]
if (!attempt || !task || task.projectId !== attempt.projectId) return false
if (attempt.workflowDefinition && Object.values(state.projectExecutions ?? {}).some(row => row.attemptId === attemptId && ['queued', 'running', 'working', 'paused', 'legacy'].includes(row.status))) return false
const leases = Object.values(state.projectResourceLeases ?? {}).filter(row => row.attemptId === attemptId && row.role !== 'chief')
const sdk = Object.values(state.projectHarnessWorkers ?? {}).filter(row => row.attemptId === attemptId && row.role !== 'chief')
const native = Object.values(state.projectNativeWorkers ?? {}).filter(row => row.attemptId === attemptId && row.role !== 'chief')
if (Object.values(state.bots ?? {}).some(row => row.ephemeral && row.attemptId === attemptId && row.role !== 'chief')) return false
if (sdk.some(row => !confirmedWorkerExit(state, row) || !leases.some(lease => lease.id === row.leaseId && lease.status === 'released' && lease.generation === row.generation && lease.projectId === row.projectId && lease.resourceId === row.resourceId && lease.role === row.role && lease.worker?.handle === row.id))) return false
if (native.some(row => !nativeExitConfirmed(state, row))) return false
if (leases.some(row => row.status !== 'released' || row.worker && !['sdk', 'native'].includes(row.worker.kind) || row.worker?.kind === 'sdk' && !sdk.some(worker => worker.id === row.worker.handle) || row.worker?.kind === 'native' && !native.some(worker => worker.id === row.worker.botId) || !row.worker && row.releaseProof?.kind !== 'unbound-reservation-release')) return false
const sessions = [...Object.values(attempt.roleSessions ?? {}), ...(attempt.roleSessionHistory ?? [])]
if (sessions.some(row => row.role !== 'chief' && row.botId && !sdk.some(worker => worker.id === row.botId) && !native.some(worker => worker.id === row.botId))) return false
const claim = task.collaboration?.claim
if (claim?.workerId && claim.role !== 'chief' && !claim.chiefRunId && !sdk.some(row => row.id === claim.workerId) && !native.some(row => row.id === claim.workerId)) return false
if (claim?.status === 'active' && !claim.workerId && !claim.chiefRunId) return false
// All adapters register before external execution; no records means no worker started.
return true
}
export function capacitySuspended(state, attempt) {
const task = state.tasks?.[attempt.taskId]
return Boolean(attempt.capacityWait && attempt.capacityWait.status !== 'admitted' && task?.deliveryAttemptId === attempt.id && attemptIdle(state, attempt.id))
}
7 changes: 6 additions & 1 deletion core/src/host/project-pools.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export function createProjectPools(rt) {
}
}
row.contextRefs = { source: contextReference(rt.state, task, fromRole), target: contextReference(rt.state, task, target.role) }
await rt.projectScheduler?.captureWait?.(task)
const before = rt.projectEffects ? { collaboration: clone(c), handoff: task.handoff, question: task.chiefQuestion } : null
let input, intent
try {
Expand Down Expand Up @@ -238,7 +239,10 @@ export function createProjectPools(rt) {
definition = definitions(task.projectId).roles.find(row => row.id === botId)
if (!definition) { c.status = 'blocked'; c.block = { reason: 'Target role is retired; Chief must explicitly select an active role', at: Date.now() }; await rt.save(); return null }
if (Object.values(rt.state.tasks).some(row => row.collaboration?.claim?.botId === botId && row.collaboration.claim.status === 'active')) return null
if (adapter.ready && !adapter.ready({ task, definition })) return null
if (adapter.ready && !await adapter.ready({ task, definition })) return null
if (rt.projectScheduler?.admitPool && !await rt.projectScheduler.admitPool(task, definition)) return null
await authority(task)
if (c.generation !== expectedGeneration || c.status !== 'queued' || c.botId !== botId) return null
const receipt = c.history.find(row => row.id === c.currentHandoffId)
if (!receipt || receipt.status !== 'pending') throw refuse('Task has no pending handoff receipt')
const limit = rt.config.projectHandoff?.quota?.steps
Expand Down Expand Up @@ -523,6 +527,7 @@ export function createProjectPools(rt) {
if (existing && existing.id !== c.attemptId && existing.feedback?.previousAttemptId === c.attemptId && rt.state.projectAttempts[c.attemptId]?.supersededBy === existing.id) c.attemptId = existing.id
else c.attemptId = await adapter.rework?.({ task, claim: c.claim, reason: `Integrate adjudicated child results: ${wait.resumePoint}` }) ?? c.attemptId
}
await rt.projectScheduler?.captureWait?.(task, { kind: 'children', id: wait.id })
const previous = wait.status
wait.status = 'queued'
try {
Expand Down
Loading
Loading