Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
066b11a
Add Project repository associations and non-destructive Owner closure
zy95-12 Nov 2, 2026
61e0abc
Separate personal-agent identities from Project participations
zy95-12 Nov 2, 2026
6eeba41
Authenticate scoped personal-agent connections with existing delegati…
zy95-12 Nov 2, 2026
b4fa8de
Preserve Task-role definitions and latest SDK session across retries
zy95-12 Nov 2, 2026
439a41c
Expose durable handoff contracts and private consultation flows
zy95-12 Nov 2, 2026
a9ff63b
Bound role context and retain scoped memory and working summaries
zy95-12 Nov 2, 2026
6ebc706
Cover scoped memory source pages and record real SDK continuation val…
zy95-12 Nov 2, 2026
4101275
Deliver fresh native Chief job context in the resident Session turn
zy95-12 Nov 2, 2026
dba40f8
Require approved durable Project role changes
zy95-12 Nov 3, 2026
3e90554
Integrate approved role governance with personal agent identities
zy95-12 Nov 3, 2026
3629495
Integrate approved roles with current personal agent authorization
zy95-12 Nov 3, 2026
cbab78a
Merge branch 'feat/2183-agent-auth' into feat/2184-execution-lifecycle
zy95-12 Nov 3, 2026
a11696d
Require native execution quiescence before releasing Task context
zy95-12 Nov 3, 2026
f404153
Execute custom Project roles with pinned capability policies
zy95-12 Nov 3, 2026
32f7941
Activate exact native context before resuming pool handoffs
zy95-12 Nov 3, 2026
3f7fe22
Require consistent native exit proof across pool and supervisor recovery
zy95-12 Nov 3, 2026
29f0e02
Revalidate retained role execution and stop revoked claims through ex…
zy95-12 Nov 3, 2026
36210b9
Compose current execution and role governance with durable handoff co…
zy95-12 Nov 3, 2026
c16a25e
Compose bounded role context with current native execution and govern…
zy95-12 Nov 3, 2026
2ad0254
Sync delivery integration ancestry for #2225
zy95-12 Nov 3, 2026
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
14 changes: 14 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-03 — Budget tool results and exact execution input

Seen 1× (large-history regression). A bounded initial context does not bound a
context tool returning every Task or note. Return a scoped index and hash/cursor
pages, preserve required constraints, and preflight the exact native/SDK input.
Aggregate SDK usage is not a measurement of current window occupancy. Structured
native handoff input must remain JSON rather than `[object Object]`.

### 2026-11-02 — Additional approved merge inputs need a host contract

Seen 1×. A published upstream already in the old candidate does not imply there
Expand Down Expand Up @@ -645,6 +653,8 @@ admission correctly refused. Mark only that completed Chief claim waiting-human;
retain actual exit/lease checks and every other claim. A regression reproduces the
old active label and verifies Task/Attempt/generation and blocked state survive.

2026-11-03 Seen 1×: a resident native Session may retain its system definition after bot metadata changes. Put each current job protocol/facts in the actual turn input; keep prior context historical and preserve the Session. The explicit Task scope fence caught stale-job output during #2179 admission verification.


### 2026-11-03 — Public Chief cannot borrow its approver's identity

Expand Down Expand Up @@ -719,3 +729,7 @@ Seen 1× (native pre-step and existing scheduler regressions). Admission-time ri
### 2026-11-03 — Revalidate handoff targets after exit inspection

Seen 1× (composed role governance/contract regression). Confirming an old source exit yields to concurrent governance. Re-read the committed destination before acquiring a new claim; retirement keeps the Task blocked instead of authorizing a stale role.

### 2026-11-03 — Logical role IDs are not execution carrier IDs

Seen 1× (composed actual DSH native input failure). A canonical claim keeps the logical Bot in botId and the temporary executor in workerId. Context input fences must compare the carrier with workerId; conflating them sends healthy work to Chief admission failure before any execution.
18 changes: 18 additions & 0 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,21 @@ an authorized native watch. External clients have isolated mailboxes, not fabric
provider sessions. SDK process/HTTP tests and real-provider acceptance remain distinct.
Native restoration/context bounds, transaction events and parent waits stay on their
original dependent issues; no control-plane, deployment or frontend changes.


### Context/memory convergence — 2026-11-03 (#2177)

Unify model/Harness context budgeting and bounded source tools across persistent
Chief and temporary roles. Unknown capacity stays explicit; full rules/current
constraints remain required, retrieval yields before overflow, and native/SDK input
is checked before delivery. Working summaries use original public role-memory
records with exact source/evidence/uncertainty, separate resident Chief topics and
rollback of uncommitted staging. Facts have source/classification/version/archive;
role-authored experience cannot declare facts or overwrite human control. Private
notes retain history under bounded views. Codex tool history has an explicit cap;
SDK compaction remains runtime-managed, without claiming aggregate usage is current
window pressure. No new memory platform, executor or timer. Native Task-role physical
restore2184 and normalized SQL storage2182 remain separate acceptance; real SDK
verification must name exact runtime source hashes, not borrow deterministic tests.

Follow-up #2177: native resident Chief current facts are actual per-turn input with whole-input budget and final authority checks; do not rely on updating cached system prompt metadata. Preserve the original resident Session.
18 changes: 18 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,24 @@ provider sessions. SDK process/HTTP tests and real-provider acceptance remain di
Native restoration/context bounds, transaction events and parent waits stay on their
original dependent issues; no control-plane, deployment or frontend changes.


### Context/memory convergence — 2026-11-03 (#2177)

Unify model/Harness context budgeting and bounded source tools across persistent
Chief and temporary roles. Unknown capacity stays explicit; full rules/current
constraints remain required, retrieval yields before overflow, and native/SDK input
is checked before delivery. Working summaries use original public role-memory
records with exact source/evidence/uncertainty, separate resident Chief topics and
rollback of uncommitted staging. Facts have source/classification/version/archive;
role-authored experience cannot declare facts or overwrite human control. Private
notes retain history under bounded views. Codex tool history has an explicit cap;
SDK compaction remains runtime-managed, without claiming aggregate usage is current
window pressure. No new memory platform, executor or timer. Native Task-role physical
restore2184 and normalized SQL storage2182 remain separate acceptance; real SDK
verification must name exact runtime source hashes, not borrow deterministic tests.

#2177 follow-up: validate two sequential native Chief jobs against fresh per-turn facts while retaining the same resident Session; dependent Task admission runtime acceptance remains #2179.

Native prior-host reconciliation now reuses verified supervisor process-group facts; native v2 registered-work/lineage and shared adapter-exit regressions guard against premature initiative transfer. In-flight current-rights abort and composed-stack acceptance remain active.

Canonical ongoing role/resource authority checks now reuse native step/SDK input and the existing scheduler + pool stop. Frozen role context remains stable; current revoked rights stop execution, with unknown exit held. Compose descendants and verify parent Task resume/API projections next.
2 changes: 2 additions & 0 deletions changelog.d/2177.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add configured model/Harness context budgets, scoped paged retrieval, source-bound Task-role/Chief working summaries and versioned role memory. Preserve private history, keep execution experience unverified, fence exact native/SDK input and bound Codex tool history.
Resident native Chief jobs receive current scoped facts in the actual per-turn input, retaining the same Session and avoiding cached-system-prompt staleness between jobs. Compose current role governance/native lifecycle without dropping scoped context budgets; fence native input by the actual worker and preserve complete mandatory policy within its unchanged cap through linked historical memory.
1 change: 1 addition & 0 deletions core/src/host/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,7 @@ export function install(rt) {
poolResult(row) { return plugin === 'flybot-workflow' ? rt.projectDevelopment.roleResult(row) : false },
spawn(spec) { return plugin === 'flybot-workflow' ? rt.projectHarness?.spawn(spec) : null },
dispatch(step) { return plugin === 'flybot-workflow' ? rt.projectHarness?.dispatch(step) : null },
checkInput(step) { return plugin === 'flybot-workflow' ? rt.projectHarness?.checkNativeInput(step) : null },
release(id) { return plugin === 'flybot-workflow' ? rt.projectHarness?.release(id) : false },
onResult(fn) { return plugin === 'flybot-workflow' ? rt.projectHarness?.onResult(fn) : () => {} },
},
Expand Down
8 changes: 5 additions & 3 deletions core/src/host/memory.js
Original file line number Diff line number Diff line change
Expand Up @@ -1490,13 +1490,15 @@ export function install(rt) {
}

// Project adapters bind the Bot themselves; this seam never resolves team/other scopes.
function botMemoryRecall(botId, { topic, query } = {}) {
function botMemoryRecall(botId, { topic, query, offset = 0, limit = 40 } = {}) {
if (rt.botOf(botId) === undefined) throw new Error('That Bot is gone')
const parsed = topicArg(topic)
if (parsed.error) throw new Error(parsed.error)
const files = loadScope(ownScope(botId)).filter(file => parsed.topic === undefined || file.topic === parsed.topic)
const entries = allEntries(files)
return { ...memoryDigest(botId), entries: (query ? matchEntries(entries, String(query).slice(0, 200)) : entries).slice(0, 40).map(entry => ({ text: entry.text, meta: entry.meta ?? {}, topic: entry.file.topic ?? null })) }
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 40) throw new Error('Invalid own-memory page')
const entries = allEntries(files), matched = query ? matchEntries(entries, String(query).slice(0, 200)) : entries
return { ...memoryDigest(botId), total: matched.length, offset, nextOffset: offset + limit < matched.length ? offset + limit : null,
entries: matched.slice(offset, offset + limit).map(entry => ({ text: entry.text, meta: entry.meta ?? {}, topic: entry.file.topic ?? null })) }
}
function botMemoryRemember(botId, { text, topic, replace } = {}, source = {}) {
const bot = rt.botOf(botId)
Expand Down
81 changes: 81 additions & 0 deletions core/src/host/project-agent-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Context tools read current authorized sources in bounded pages. Their text is
// data, never a publication grant, a role definition, or proof of completed work.
import { ownEntry } from './store.js'
import { contextValuePage, fitContextToolResult, resolveContextBudget } from './project-context-budget.js'
import { readContextArchive } from './project-role-context.js'
const fail = message => Object.assign(new Error(message), { code: 'bot/context-budget' })
const forbidden = message => Object.assign(new Error(message), { code: 'bot/forbidden' })
const taskIndex = row => ({ id: row.id, projectId: row.projectId, title: row.title, status: row.status, criteria: row.criteria ?? '' })
async function budgetFor(rt, agent) {
if (agent.lastRequest?.context?.budget) return agent.lastRequest.context.budget
const bot = rt.botOf(agent.botId)
return resolveContextBudget(rt, { model: bot?.model ?? rt.config.projectModel ?? rt.cachedDefault?.() })
}
export async function exchangeProjectContext(rt, agentId, delegationId, { contextRequests = [], memoryUpdates = [], memoryRequests = [] }, snapshot, { authorize, isDelegationActive }) {
const agent = ownEntry(rt.state.projectAgents ?? {}, agentId)
if (!agent || agent.id !== `public-${agent.projectId}` || agent.visibility !== 'public' || !isDelegationActive(agentId, delegationId)) throw forbidden('Chief context delegation is unavailable')
const authority = await authorize(rt.botOf(agent.botId))
if (authority.delegation.readOnly && memoryUpdates.length) throw forbidden('Read-only conversations cannot publish memory')
if (snapshot?.archive?.projectId !== agent.projectId || snapshot.archive.role !== 'chief') throw forbidden('Context belongs to another Project or role')
if (![contextRequests, memoryUpdates, memoryRequests].every(rows => Array.isArray(rows) && rows.length <= 4)) throw fail('Chief context operations exceed their budget')
const budget = await budgetFor(rt, agent)
const reads = contextRequests.length ? readContextArchive(snapshot, contextRequests, { projectId: agent.projectId, role: 'chief' }) : []
const memories = memoryRequests.map(request => {
if (!rt.botMemoryRecall) throw forbidden('Bot memory is unavailable')
const found = rt.botMemoryRecall(agent.botId, { topic: request.topic, query: request.query, offset: request.offset ?? 0, limit: request.limit ?? 4 })
return { topic: request.topic ?? null, query: request.query ?? null, offset: found.offset, total: found.total, nextOffset: found.nextOffset,
entries: found.entries.map((entry, index) => ({ ...contextValuePage(entry, budget, { offset: request.charOffset ?? 0, sha256: request.sha256 }), entryOffset: found.offset + index, topic: entry.topic, ...(request.charOffset ? {} : { meta: entry.meta }) })) }
})
// A mutation must not succeed and then disappear behind an oversized tool
// response. Validate the response envelope before writing durable memory.
fitContextToolResult({ reads, memories, writes: memoryUpdates.map(() => ({ changed: true, message: 'Host-confirmed memory write'.padEnd(512, '.') })) }, budget)
const writes = []
for (const request of memoryUpdates) {
if (!rt.botMemoryRemember) throw forbidden('Bot memory is unavailable')
const result = await rt.botMemoryRemember(agent.botId, { text: request.text, topic: request.topic, replace: request.replace }, { reference: `Project ${agent.projectId}; Chief request ${agent.lastRequest.id}` })
writes.push({ changed: result.changed, message: result.message })
}
return fitContextToolResult({ reads, memories, writes }, budget)
}
export async function readProjectAgentContext(rt, bot, body, { authorize, contextExchange, privateNotes, logOf, rooms, chiefMemory }) {
const { agent, actor, delegation } = await authorize(bot)
if (body.contextRequests || body.memoryRequests || body.memoryUpdates) return contextExchange(agent.id, delegation.id, body, agent.lastRequest.context)
const budget = await budgetFor(rt, agent)
const detail = await rt.projectEndpoint('project-detail', { id: agent.projectId }, actor)
const notes = agent.visibility === 'private' && !agent.returnAgentId ? ownEntry(privateNotes(), agent.id) ?? [] : []
const messages = agent.roomId ? rooms.requireRoom(agent.projectId, agent.roomId, actor).messages : ownEntry(rt.state.projectAgentTranscripts ?? {}, `public-${agent.projectId}`) ?? []
const source = body.sourceRequest
if (source !== undefined) {
if (!source || typeof source !== 'object' || Array.isArray(source)) throw forbidden('Invalid scoped context source')
let value
if (source.kind === 'project') value = detail.project
else if (source.kind === 'task-index') value = detail.tasks.filter(row => !source.query || String(row.title ?? '').toLowerCase().includes(String(source.query).toLowerCase())).map(({ id, title, status }) => ({ id, title, status }))
else if (source.kind === 'task') {
const task = ownEntry(rt.state.tasks, source.id)
if (task?.projectId !== agent.projectId) throw forbidden('Task source belongs to another Project')
value = { ...taskIndex(task), state: rt.projectDevelopment.view(task, actor) }
} else if (source.kind === 'private-memory') value = notes.find(row => row.id === source.id)
else if (source.kind === 'public-message') value = messages.find(row => row.id === source.id)
else if (source.kind === 'knowledge') value = (ownEntry(rt.state.projectKnowledge ?? {}, agent.projectId) ?? []).find(row => row.id === source.id)
else if (source.kind === 'role-memory') {
const row = ownEntry(rt.state.projectRoleMemory ?? {}, source.id)
if (row?.projectId !== agent.projectId) throw forbidden('Role memory source belongs to another Project')
const { versions = [], ...head } = row
value = source.revision === undefined || source.revision === row.revision ? head : versions.find(version => version.revision === source.revision)
} else throw forbidden('Unknown context source kind')
if (!value) throw forbidden('Context source is unavailable in this scope')
return fitContextToolResult({ identity: { visibility: agent.visibility, projectId: agent.projectId, memberId: actor.memberId }, contextPage: contextValuePage(value, budget, source) }, budget)
}
const sources = agent.lastRequest?.context?.sources
const result = { permissionMode: delegation.readOnly ? 'conversation' : actor.watchGrantId ? 'watch' : 'safe', project: detail.project,
tasks: detail.tasks.slice(-20).map(taskIndex), runs: detail.runs.slice(-10).map(({ id, taskId, status }) => ({ id, taskId, status })),
knowledge: (ownEntry(rt.state.projectKnowledge ?? {}, agent.projectId) ?? []).slice(-10),
contextSources: sources ? { policy: sources.policy ?? null, counts: { memory: sources.memory?.length ?? 0, knowledge: sources.knowledge?.length ?? 0, repository: sources.repository?.length ?? 0 }, archiveSha256: agent.lastRequest.context.archiveSha256 } : null,
publicMessages: messages.slice(-10).map(({ id, kind, text, at }) => ({ id, kind, text, at })),
...(agent.visibility === 'public' && !agent.roomId ? { chiefMemory: { ...chiefMemory(agent.projectId), topics: chiefMemory(agent.projectId).topics.slice(0, 12) } } : {}),
...(agent.visibility === 'private' ? { privateMemory: notes.slice(-20), privateConversation: logOf(agent.id).slice(-10).filter(row => typeof row.text === 'string').map(({ id, kind, text, at }) => ({ id, kind, text, at })) } : {}),
identity: { visibility: agent.visibility, projectId: agent.projectId, memberId: actor.memberId },
retrieval: { version: 'project-context-index/v1', omitted: {}, counts: { tasks: detail.tasks.length, privateMemory: notes.length, publicMessages: messages.length },
instruction: 'This is a bounded index. Use sourceRequest:{kind:project|task-index|task|private-memory|public-message|knowledge|role-memory,id,offset,sha256} for exact authorized sources; source pages retain hashes/cursors. Chief contextRequests reads its immutable archive, including summary sources; memoryRequests reads its own durable topic pages.' } }
return fitContextToolResult(result, budget)
}
Loading
Loading