diff --git a/.agents/skills/linear/queries/issue_by_title.graphql b/.agents/skills/linear/queries/issue_by_title.graphql
new file mode 100644
index 000000000..5dab6ffb4
--- /dev/null
+++ b/.agents/skills/linear/queries/issue_by_title.graphql
@@ -0,0 +1,5 @@
+query IssueByTitle($title: String!, $teamId: ID!) {
+ issues(filter: { title: { eq: $title }, team: { id: { eq: $teamId } } }, first: 2) {
+ nodes { id identifier title }
+ }
+}
diff --git a/.agents/skills/linear/queries/issue_delete.graphql b/.agents/skills/linear/queries/issue_delete.graphql
new file mode 100644
index 000000000..5b85bb89a
--- /dev/null
+++ b/.agents/skills/linear/queries/issue_delete.graphql
@@ -0,0 +1,5 @@
+mutation IssueDelete($id: String!) {
+ issueDelete(id: $id) {
+ success
+ }
+}
diff --git a/.agents/skills/linear/queries/issue_label_delete.graphql b/.agents/skills/linear/queries/issue_label_delete.graphql
new file mode 100644
index 000000000..dd4c6434b
--- /dev/null
+++ b/.agents/skills/linear/queries/issue_label_delete.graphql
@@ -0,0 +1,5 @@
+mutation IssueLabelDelete($id: String!) {
+ issueLabelDelete(id: $id) {
+ success
+ }
+}
diff --git a/.agents/skills/linear/queries/project_by_name.graphql b/.agents/skills/linear/queries/project_by_name.graphql
new file mode 100644
index 000000000..76d30326d
--- /dev/null
+++ b/.agents/skills/linear/queries/project_by_name.graphql
@@ -0,0 +1,5 @@
+query ProjectByName($name: String!) {
+ projects(filter: { name: { eq: $name } }, first: 2) {
+ nodes { id name slugId }
+ }
+}
diff --git a/.agents/skills/linear/queries/project_create.graphql b/.agents/skills/linear/queries/project_create.graphql
new file mode 100644
index 000000000..b8be8002b
--- /dev/null
+++ b/.agents/skills/linear/queries/project_create.graphql
@@ -0,0 +1,10 @@
+mutation ProjectCreate($input: ProjectCreateInput!) {
+ projectCreate(input: $input) {
+ success
+ project {
+ id
+ name
+ slugId
+ }
+ }
+}
diff --git a/.agents/skills/linear/queries/project_delete.graphql b/.agents/skills/linear/queries/project_delete.graphql
new file mode 100644
index 000000000..45ebe8a23
--- /dev/null
+++ b/.agents/skills/linear/queries/project_delete.graphql
@@ -0,0 +1,5 @@
+mutation ProjectDelete($id: String!) {
+ projectDelete(id: $id) {
+ success
+ }
+}
diff --git a/.agents/skills/linear/queries/team_states.graphql b/.agents/skills/linear/queries/team_states.graphql
new file mode 100644
index 000000000..d38b87317
--- /dev/null
+++ b/.agents/skills/linear/queries/team_states.graphql
@@ -0,0 +1,8 @@
+query TeamStates($id: String!) {
+ team(id: $id) {
+ id
+ states {
+ nodes { id name type }
+ }
+ }
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 03cc392a1..611601fa6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -21,6 +21,16 @@ jobs:
run: cargo clippy --workspace --all-targets -- -D warnings
- name: test
run: cargo test --workspace
+ acp-windows:
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@1.97.1
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - name: ACP Windows process ownership regressions
+ run: python scripts/validation/check-acp-windows.py
typescript:
runs-on: ubuntu-latest
steps:
diff --git a/.opensymphony/memory/memory.yaml b/.opensymphony/memory/memory.yaml
index 96e98d289..af2cccab1 100644
--- a/.opensymphony/memory/memory.yaml
+++ b/.opensymphony/memory/memory.yaml
@@ -29,6 +29,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -37,6 +38,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -44,6 +46,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -56,11 +59,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -70,6 +77,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -83,8 +91,32 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
+ acp:
+ title: Acp
+ docs_target: docs/acp.md
+ visibility: public
+ status: stable
+ confidence: 95
+ aliases:
+ - acp
+ source_refs:
+ linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:orchestrator
+ - area:runtime
+ - area:testing
+ - area:ui
+ - area:workspace
+ linear_milestones:
+ - 'M12.99: ACP Harness Runtime Adapter'
ai-pr-review-human-setup:
title: OpenHands PR Review - Human Setup Guide
docs_target: docs/ai-pr-review-human-setup.md
@@ -126,6 +158,8 @@ areas:
- 'M12.6: Tree-sitter Code Intelligence'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M6: Gateway And Stream Contract'
- 'M7: Shared Client And Desktop Alpha'
@@ -143,6 +177,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -163,6 +198,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -178,9 +214,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -214,6 +253,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -230,6 +271,7 @@ areas:
- docs/cli.md
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -250,6 +292,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -265,9 +308,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -301,6 +347,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -316,6 +364,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -324,6 +373,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -331,6 +381,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -343,11 +394,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -357,6 +412,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -370,6 +426,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
code-review:
@@ -384,6 +442,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -392,6 +451,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -399,6 +459,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -411,11 +472,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -425,6 +490,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -439,6 +505,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
codex:
@@ -452,6 +520,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -472,6 +541,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -487,9 +557,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -523,6 +596,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -560,6 +635,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -568,6 +644,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -575,6 +652,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -588,11 +666,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -602,6 +684,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -616,6 +699,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
configuration:
@@ -631,6 +716,7 @@ areas:
- docs/configuration.md
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -639,6 +725,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -646,6 +733,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -658,11 +746,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -672,6 +764,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -691,6 +784,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M7: Shared Client And Desktop Alpha'
@@ -708,6 +803,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -728,6 +824,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -743,9 +840,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -779,6 +879,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -793,20 +895,37 @@ areas:
- data-contracts
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
- area:data-contracts
+ - area:docs
- area:evidence
- area:experiments
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
- area:outcomes
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
+ - area:recovery
+ - area:review
- area:runner
+ - area:runtime
- area:scheduler
- area:task-packets
+ - area:testing
+ - area:ui
- area:verifiers
- area:workspace
+ - area:workspace-lifecycle
linear_milestones:
- 'M0: Foundation And Evidence Contracts'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
debugging:
title: Debugging
@@ -819,6 +938,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -827,6 +947,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -834,6 +955,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -846,11 +968,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -860,6 +986,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -873,6 +1000,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
deployment-modes:
@@ -901,6 +1030,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -921,6 +1051,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -936,9 +1067,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -972,6 +1106,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -987,6 +1123,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -1007,6 +1144,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -1022,9 +1160,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -1057,6 +1198,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -1110,6 +1253,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -1130,6 +1274,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -1145,9 +1290,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -1181,6 +1329,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -1195,6 +1345,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -1203,6 +1354,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -1210,6 +1362,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -1222,11 +1375,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -1236,6 +1393,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -1250,6 +1408,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
duckdb:
@@ -1263,6 +1423,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -1283,6 +1444,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -1298,9 +1460,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -1334,6 +1499,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -1358,19 +1525,36 @@ areas:
- evidence
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
- area:evidence
- area:experiments
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
- area:outcomes
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
+ - area:recovery
+ - area:review
- area:runner
+ - area:runtime
- area:scheduler
- area:task-packets
+ - area:testing
+ - area:ui
- area:verifiers
- area:workspace
+ - area:workspace-lifecycle
linear_milestones:
- 'M0: Foundation And Evidence Contracts'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
experiments:
title: Experiments
@@ -1382,9 +1566,27 @@ areas:
- experiments
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
- area:experiments
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
- area:scheduler
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
frontend:
title: Frontend
@@ -1397,6 +1599,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -1405,6 +1608,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -1412,6 +1616,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -1424,11 +1629,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -1438,6 +1647,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -1451,6 +1661,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
gateway:
@@ -1464,6 +1676,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -1484,6 +1697,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -1499,9 +1713,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -1535,6 +1752,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -1550,6 +1769,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -1558,6 +1778,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -1565,6 +1786,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -1577,11 +1799,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -1591,6 +1817,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -1604,8 +1831,39 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
+ harness:
+ title: Harness
+ docs_target: docs/harness.md
+ visibility: public
+ status: stable
+ confidence: 95
+ aliases:
+ - harness
+ source_refs:
+ linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
+ linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
harness-runtime:
title: Harness Runtime
docs_target: docs/harness-runtime.md
@@ -1618,6 +1876,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -1638,6 +1897,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -1653,9 +1913,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -1688,6 +1951,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -1780,6 +2045,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -1800,6 +2066,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -1815,9 +2082,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -1850,6 +2120,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -1864,6 +2136,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -1884,6 +2157,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -1899,9 +2173,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -1935,6 +2212,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -1964,6 +2243,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -1984,6 +2264,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -1999,9 +2280,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -2034,6 +2318,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -2054,6 +2340,7 @@ areas:
- docs/memory.md
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -2074,6 +2361,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -2089,9 +2377,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -2125,6 +2416,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M6.5: Memory Server'
@@ -2144,6 +2437,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -2152,6 +2446,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -2159,6 +2454,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -2171,11 +2467,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -2185,6 +2485,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -2198,6 +2499,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
migration-1-0-0:
@@ -2223,6 +2526,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -2243,6 +2547,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -2258,9 +2563,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -2293,6 +2601,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -2307,6 +2617,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -2327,6 +2638,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -2342,9 +2654,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -2377,6 +2692,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -2420,6 +2737,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -2428,6 +2746,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -2435,6 +2754,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -2447,11 +2767,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -2461,6 +2785,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -2475,6 +2800,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
operations:
@@ -2490,6 +2817,7 @@ areas:
- docs/operations.md
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -2510,6 +2838,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -2525,9 +2854,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -2562,6 +2894,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M6.5: Memory Server'
@@ -2581,6 +2915,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -2601,6 +2936,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -2616,9 +2952,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -2652,6 +2991,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -2665,9 +3006,27 @@ areas:
- outcomes
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
- area:outcomes
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
+ - area:testing
+ - area:ui
- area:verifiers
+ - area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
planning:
title: Planning
@@ -2680,6 +3039,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -2688,6 +3048,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -2695,6 +3056,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -2707,11 +3069,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -2721,6 +3087,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -2734,6 +3101,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
prerequisites:
@@ -2764,9 +3133,27 @@ areas:
- process-protocol
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
- area:process-protocol
+ - area:provider
+ - area:recovery
+ - area:review
- area:runner
+ - area:runtime
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
provenance:
title: Provenance
@@ -2778,10 +3165,55 @@ areas:
- provenance
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
- area:provenance
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
- area:task-packets
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
+ provider:
+ title: Provider
+ docs_target: docs/provider.md
+ visibility: public
+ status: stable
+ confidence: 95
+ aliases:
+ - provider
+ source_refs:
+ linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:orchestrator
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
+ linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
quality:
title: Quality
docs_target: docs/quality.md
@@ -2792,19 +3224,36 @@ areas:
- quality
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
- area:evidence
- area:experiments
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
- area:outcomes
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
+ - area:recovery
+ - area:review
- area:runner
+ - area:runtime
- area:scheduler
- area:task-packets
+ - area:testing
+ - area:ui
- area:verifiers
- area:workspace
+ - area:workspace-lifecycle
linear_milestones:
- 'M0: Foundation And Evidence Contracts'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
query-packs:
title: Query Packs
@@ -2818,6 +3267,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -2826,6 +3276,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -2833,6 +3284,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -2845,11 +3297,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -2859,6 +3315,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -2872,8 +3329,35 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
+ recovery:
+ title: Recovery
+ docs_target: docs/recovery.md
+ visibility: public
+ status: stable
+ confidence: 95
+ aliases:
+ - recovery
+ source_refs:
+ linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:orchestrator
+ - area:recovery
+ - area:runtime
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
+ linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
release:
title: Release
docs_target: docs/release.md
@@ -2885,6 +3369,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -2893,6 +3378,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -2900,6 +3386,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -2912,11 +3399,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -2926,6 +3417,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -2940,6 +3432,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
repository-layout:
@@ -2959,7 +3453,34 @@ areas:
linear_milestones:
- 'M10.3: Codex And Subscription Readiness'
- 'M12.6: Tree-sitter Code Intelligence'
+ - 'M12.96: Parent Integration Lifecycle'
- 'M9.5: Developer Build Acceleration'
+ review:
+ title: Review
+ docs_target: docs/review.md
+ visibility: public
+ status: stable
+ confidence: 95
+ aliases:
+ - review
+ source_refs:
+ linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:orchestrator
+ - area:recovery
+ - area:review
+ - area:runtime
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
+ linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
run-detail:
title: Run Detail
docs_target: docs/run-detail.md
@@ -2972,6 +3493,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -2980,6 +3502,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -2987,6 +3510,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -2999,11 +3523,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -3013,6 +3541,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -3026,6 +3555,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
runner:
@@ -3038,8 +3569,26 @@ areas:
- runner
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
+ - area:provider
+ - area:recovery
+ - area:review
- area:runner
+ - area:runtime
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
runtime:
title: Runtime
@@ -3052,6 +3601,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -3072,6 +3622,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -3087,9 +3638,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -3123,6 +3677,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -3137,6 +3693,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -3145,6 +3702,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -3152,6 +3710,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -3164,11 +3723,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -3178,6 +3741,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -3192,6 +3756,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
scheduler:
@@ -3204,8 +3770,26 @@ areas:
- scheduler
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
- area:scheduler
+ - area:testing
+ - area:ui
+ - area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
security:
title: Security
@@ -3218,6 +3802,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -3238,6 +3823,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -3253,9 +3839,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -3289,6 +3878,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -3312,6 +3903,7 @@ areas:
- 'M12.6: Tree-sitter Code Intelligence'
- 'M12.85: Codex Thread Lifecycle'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
- 'M6: Gateway And Stream Contract'
- 'M9: Collaborative Planning Alpha'
@@ -3338,6 +3930,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -3346,6 +3939,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -3353,6 +3947,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -3365,11 +3960,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -3379,6 +3978,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -3392,6 +3992,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
task-packets:
@@ -3405,9 +4007,26 @@ areas:
- task-packets
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
- area:task-packets
+ - area:testing
+ - area:ui
- area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
template:
title: Template
@@ -3420,6 +4039,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -3428,6 +4048,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -3435,6 +4056,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -3447,11 +4069,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -3461,6 +4087,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -3474,6 +4101,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
testing:
@@ -3487,6 +4116,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -3507,6 +4137,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -3522,9 +4153,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -3558,6 +4192,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -3586,6 +4222,8 @@ areas:
- 'M12.6: Tree-sitter Code Intelligence'
- 'M12.85: Codex Thread Lifecycle'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M6: Gateway And Stream Contract'
- 'M7: Shared Client And Desktop Alpha'
@@ -3603,6 +4241,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -3611,6 +4250,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -3618,6 +4258,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -3630,11 +4271,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -3644,6 +4289,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -3658,6 +4304,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
threejs:
@@ -3671,6 +4319,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -3679,6 +4328,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -3686,6 +4336,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -3698,11 +4349,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -3712,6 +4367,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -3725,6 +4381,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
tui:
@@ -3738,6 +4396,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -3758,6 +4417,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -3773,9 +4433,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -3808,6 +4471,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -3822,6 +4487,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:build
- area:cli
- area:code-intelligence
@@ -3842,6 +4508,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:harness-runtime
- area:installer
- area:linear
@@ -3857,9 +4524,12 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
- area:runtime
@@ -3892,6 +4562,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
- 'M9.5: Developer Build Acceleration'
@@ -3920,8 +4592,26 @@ areas:
- verifiers
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
+ - area:testing
+ - area:ui
- area:verifiers
+ - area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
web:
title: Web
@@ -3934,6 +4624,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -3942,6 +4633,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -3949,6 +4641,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -3961,11 +4654,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -3975,6 +4672,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -3988,6 +4686,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
websocket-runtime:
@@ -4016,6 +4716,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -4024,6 +4725,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -4031,6 +4733,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -4043,11 +4746,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -4057,6 +4764,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -4071,6 +4779,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
workspace:
@@ -4083,8 +4793,25 @@ areas:
- workspace
source_refs:
linear_labels:
+ - area:acp
+ - area:cli
+ - area:configuration
+ - area:docs
+ - area:gateway
+ - area:harness
+ - area:memory
+ - area:orchestrator
+ - area:provider
+ - area:recovery
+ - area:review
+ - area:runtime
+ - area:testing
+ - area:ui
- area:workspace
+ - area:workspace-lifecycle
linear_milestones:
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M1: Task And Run Orchestration'
workspace-and-lifecycle:
title: Workspace and Lifecycle
@@ -4100,6 +4827,7 @@ areas:
- docs/workspace-and-lifecycle.md
linear_milestones:
- 'M10: Web Client And External Gateway'
+ - 'M12.99: ACP Harness Runtime Adapter'
workspace-lifecycle:
title: Workspace Lifecycle
docs_target: docs/workspace-lifecycle.md
@@ -4112,6 +4840,7 @@ areas:
source_refs:
linear_labels:
- area:accessibility
+ - area:acp
- area:cli
- area:code-intelligence
- area:code-review
@@ -4120,6 +4849,7 @@ areas:
- area:data-contracts
- area:debugging
- area:desktop
+ - area:docs
- area:documentation
- area:duckdb
- area:evidence
@@ -4127,6 +4857,7 @@ areas:
- area:frontend
- area:gateway
- area:graph-view
+ - area:harness
- area:installer
- area:linear
- area:mcp
@@ -4139,11 +4870,15 @@ areas:
- area:planning
- area:process-protocol
- area:provenance
+ - area:provider
- area:quality
- area:query-packs
+ - area:recovery
- area:release
+ - area:review
- area:run-detail
- area:runner
+ - area:runtime
- area:rust
- area:scheduler
- area:security
@@ -4153,6 +4888,7 @@ areas:
- area:testing
- area:tests
- area:threejs
+ - area:ui
- area:verifiers
- area:web
- area:workflow
@@ -4166,6 +4902,8 @@ areas:
- 'M12.85: Codex Thread Lifecycle'
- 'M12.8: Desktop App Installer And Auto-Update'
- 'M12.95: Multi-Repository Foundations'
+ - 'M12.96: Parent Integration Lifecycle'
+ - 'M12.99: ACP Harness Runtime Adapter'
- 'M12.9: Code Graph View'
- 'M1: Task And Run Orchestration'
redaction:
diff --git a/AGENTS.md b/AGENTS.md
index 07b88b64c..df03df97c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -118,6 +118,7 @@ Preferred crate and trait boundaries:
- `opensymphony-linear`
- `opensymphony-openhands`
- `opensymphony-codex`
+- `opensymphony-acp`
- `opensymphony-orchestrator`
- `opensymphony-control`
- `opensymphony-cli`
@@ -156,11 +157,19 @@ The runtime client must:
### Keep harness capability discovery public
- Public harness metadata belongs in `opensymphony-gateway-schema::capability::HarnessCapability` and the `/api/v1/capabilities` response.
-- Use stable harness kind strings such as `openhands_agent_server`, `codex_app_server`, and `rust_native`; do not expose private adapter type names to clients.
+- Use stable harness kind strings such as `openhands_agent_server`, `codex_app_server`, `acp`, and `rust_native`; do not expose private adapter type names to clients.
- Concrete harness adapters should implement the domain `HarnessAdapter` capability boundary and keep OpenHands, Codex, or future in-process protocol details inside their adapter modules.
- Future or experimental harnesses may be advertised as unavailable capability entries, but their feature gaps must be explicit.
- When changing harness capability discovery, update gateway schema round-trip tests, the gateway capabilities endpoint test, adapter-boundary tests, and `docs/harness-adapter-compatibility.md`.
+The ACP harness uses named profiles through the production `opensymphony run`
+worker backend and the retained `opensymphony_acp::SessionHost`. Keep protocol
+ownership inside that module and scheduling decisions in the orchestrator.
+Persist the selected profile and route for recovery; never replay an uncertain
+submission. ACP-only execution must not start an OpenHands server. Public ACP
+capabilities distinguish adapter support, profile preflight and negotiated run
+support; see `docs/harness-adapter-compatibility.md`.
+
The local Codex app-server harness uses the `opensymphony_codex` internal module
boundary and is advertised as an available local stdio capability. Keep Codex
app-server launch, JSON-RPC lifecycle, event-normalization, generated-contract,
diff --git a/Cargo.lock b/Cargo.lock
index 0faa8dde3..e6fa472ba 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,56 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+[[package]]
+name = "agent-client-protocol"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13f780597b2c88ab83d02509bcc3e86366751f86fa12114005e35c55ed691763"
+dependencies = [
+ "agent-client-protocol-derive",
+ "agent-client-protocol-schema",
+ "async-io",
+ "async-process",
+ "blocking",
+ "futures",
+ "futures-concurrency",
+ "rustc-hash",
+ "rustix 1.1.4",
+ "schemars 1.2.2",
+ "serde",
+ "serde_json",
+ "shell-words",
+ "tracing",
+ "uuid",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "agent-client-protocol-derive"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4893bfb11e0e193aa5a64611a126ee87567acb1879556519c902722899229de"
+dependencies = [
+ "quote",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "agent-client-protocol-schema"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d595a79e1665d02c91c7dd1bedd118501e75cc0a3d5b1651b5150ec4c5c1e0d"
+dependencies = [
+ "anyhow",
+ "derive_more",
+ "schemars 1.2.2",
+ "serde",
+ "serde_json",
+ "serde_with",
+ "strum 0.28.0",
+ "tracing",
+]
+
[[package]]
name = "ahash"
version = "0.7.8"
@@ -219,7 +269,7 @@ dependencies = [
"arrow-schema",
"arrow-select",
"atoi",
- "base64",
+ "base64 0.22.1",
"chrono",
"comfy-table",
"half",
@@ -273,7 +323,7 @@ version = "58.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f633dbfdf39c039ada1bf9e34c694816eb71fbb7dc78f613993b7245e078a1ed"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
]
[[package]]
@@ -307,6 +357,83 @@ dependencies = [
"regex-syntax",
]
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix 1.1.4",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix 1.1.4",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix 1.1.4",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "async-stream"
version = "0.3.6"
@@ -329,6 +456,12 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -369,7 +502,7 @@ checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
dependencies = [
"axum-core",
"axum-macros",
- "base64",
+ "base64 0.22.1",
"bytes",
"form_urlencoded",
"futures-util",
@@ -434,6 +567,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -449,6 +588,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
[[package]]
name = "bitflags"
version = "2.11.0"
@@ -476,6 +621,19 @@ dependencies = [
"generic-array",
]
+[[package]]
+name = "blocking"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
[[package]]
name = "borrow-or-share"
version = "0.2.4"
@@ -506,6 +664,15 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
[[package]]
name = "bumpalo"
version = "3.20.2"
@@ -647,6 +814,15 @@ dependencies = [
"unicode-width",
]
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "const-random"
version = "0.1.18"
@@ -710,13 +886,19 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
+
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"crossterm_winapi",
"parking_lot",
"rustix 0.38.44",
@@ -729,7 +911,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"crossterm_winapi",
"derive_more",
"document-features",
@@ -766,12 +948,77 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "darling"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
+dependencies = [
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 3.0.6",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 3.0.6",
+]
+
[[package]]
name = "data-encoding"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "deranged"
version = "0.5.8"
@@ -779,6 +1026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
+ "serde_core",
]
[[package]]
@@ -812,6 +1060,7 @@ dependencies = [
"quote",
"rustc_version",
"syn 2.0.117",
+ "unicode-xid",
]
[[package]]
@@ -859,9 +1108,15 @@ dependencies = [
"libduckdb-sys",
"num-integer",
"rust_decimal",
- "strum",
+ "strum 0.27.2",
]
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
[[package]]
name = "either"
version = "1.15.0"
@@ -893,6 +1148,26 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
[[package]]
name = "eventsource-stream"
version = "0.2.3"
@@ -949,6 +1224,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
[[package]]
name = "flate2"
version = "1.1.9"
@@ -1036,7 +1317,7 @@ checksum = "07f72a180b002371a31ad286c396801adc47a095df6a4f5f5d3e4f91ee0a3c06"
dependencies = [
"ahash 0.8.12",
"arc-swap",
- "bitflags",
+ "bitflags 2.11.0",
"crossterm 0.29.0",
"signal-hook 0.4.3",
"unicode-display-width",
@@ -1078,7 +1359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66559719b7cf30a9824fcb368b82e36eacee7178ef85c8bc2ec4c588dc3dfdf9"
dependencies = [
"ahash 0.8.12",
- "bitflags",
+ "bitflags 2.11.0",
"bumpalo",
"ftui-core",
"memchr",
@@ -1144,7 +1425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5cd953320541aafc80ca0546ff26a9478e161baf421d9e0372c2f503b1e881b"
dependencies = [
"ahash 0.8.12",
- "bitflags",
+ "bitflags 2.11.0",
"ftui-core",
"ftui-layout",
"ftui-render",
@@ -1161,50 +1442,102 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+[[package]]
+name = "futures"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
[[package]]
name = "futures-channel"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
"futures-sink",
]
+[[package]]
+name = "futures-concurrency"
+version = "7.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6"
+dependencies = [
+ "fixedbitset",
+ "futures-core",
+ "futures-lite",
+ "pin-project",
+ "smallvec",
+]
+
[[package]]
name = "futures-core"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
[[package]]
name = "futures-io"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
+
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
[[package]]
name = "futures-macro"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.117",
+ "syn 3.0.6",
]
[[package]]
name = "futures-sink"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
[[package]]
name = "futures-task"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-timer"
@@ -1214,10 +1547,11 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
-version = "0.3.32"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
+ "futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -1340,6 +1674,18 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hermit-abi"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
[[package]]
name = "http"
version = "1.4.0"
@@ -1430,7 +1776,7 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"futures-channel",
"futures-util",
@@ -1558,6 +1904,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
[[package]]
name = "idna"
version = "1.1.0"
@@ -1579,6 +1931,17 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -1628,6 +1991,60 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+[[package]]
+name = "jiff"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-link",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
+dependencies = [
+ "defmt",
+ "log",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
[[package]]
name = "jobserver"
version = "0.1.34"
@@ -1756,9 +2173,9 @@ dependencies = [
[[package]]
name = "libc"
-version = "0.2.183"
+version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libduckdb-sys"
@@ -1952,6 +2369,18 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "nix"
+version = "0.31.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+dependencies = [
+ "bitflags 2.11.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
[[package]]
name = "nom"
version = "7.1.3"
@@ -2077,8 +2506,11 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "opensymphony"
-version = "2.11.3"
+version = "3.0.0"
dependencies = [
+ "agent-client-protocol",
+ "agent-client-protocol-schema",
+ "aho-corasick",
"async-stream",
"async-trait",
"axum",
@@ -2092,6 +2524,7 @@ dependencies = [
"jsonschema",
"liquid",
"lru",
+ "process-wrap",
"reqwest",
"reqwest-eventsource",
"rustix 1.1.4",
@@ -2117,6 +2550,7 @@ dependencies = [
"unicode-width",
"url",
"uuid",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2125,6 +2559,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -2197,6 +2637,26 @@ dependencies = [
"sha2",
]
+[[package]]
+name = "pin-project"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -2209,12 +2669,52 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix 1.1.4",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -2267,6 +2767,19 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "process-wrap"
+version = "10.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e3f4237d0e4741eb50bc5584db701f1299c85fa31ff0274dd6445e79dc42d12"
+dependencies = [
+ "futures",
+ "indexmap 2.13.0",
+ "nix",
+ "tokio",
+ "windows",
+]
+
[[package]]
name = "ptr_meta"
version = "0.1.4"
@@ -2434,7 +2947,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
]
[[package]]
@@ -2518,7 +3031,7 @@ version = "0.12.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
@@ -2661,7 +3174,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.4.15",
@@ -2674,7 +3187,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys 0.12.1",
@@ -2749,6 +3262,43 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "schemars"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
+dependencies = [
+ "dyn-clone",
+ "ref-cast",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 3.0.6",
+]
+
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -2767,7 +3317,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -2820,13 +3370,24 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "serde_derive_internals"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
[[package]]
name = "serde_json"
-version = "1.0.149"
+version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
- "indexmap",
+ "indexmap 2.13.0",
"itoa",
"memchr",
"serde",
@@ -2866,13 +3427,46 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_with"
+version = "3.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4"
+dependencies = [
+ "base64 0.23.1",
+ "bs58",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.13.0",
+ "jiff",
+ "schemars 0.9.0",
+ "schemars 1.2.2",
+ "serde_core",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
+]
+
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
- "indexmap",
+ "indexmap 2.13.0",
"itoa",
"ryu",
"serde",
@@ -2910,6 +3504,12 @@ dependencies = [
"lazy_static",
]
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -3027,7 +3627,16 @@ version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
- "strum_macros",
+ "strum_macros 0.27.2",
+]
+
+[[package]]
+name = "strum"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
+dependencies = [
+ "strum_macros 0.28.0",
]
[[package]]
@@ -3042,6 +3651,18 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "strum_macros"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
[[package]]
name = "subtle"
version = "2.6.1"
@@ -3070,6 +3691,17 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "syn"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
[[package]]
name = "sync_wrapper"
version = "1.0.2"
@@ -3336,7 +3968,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
- "indexmap",
+ "indexmap 2.13.0",
"serde",
"serde_spanned",
"toml_datetime 0.6.11",
@@ -3350,7 +3982,7 @@ version = "0.25.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
dependencies = [
- "indexmap",
+ "indexmap 2.13.0",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"winnow 1.0.2",
@@ -3393,7 +4025,7 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"bytes",
"futures-util",
"http",
@@ -3813,7 +4445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
- "indexmap",
+ "indexmap 2.13.0",
"wasm-encoder",
"wasmparser",
]
@@ -3837,9 +4469,9 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
- "bitflags",
+ "bitflags 2.11.0",
"hashbrown 0.15.5",
- "indexmap",
+ "indexmap 2.13.0",
"semver",
]
@@ -3894,6 +4526,27 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -3907,6 +4560,17 @@ dependencies = [
"windows-strings",
]
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
[[package]]
name = "windows-implement"
version = "0.60.2"
@@ -3935,6 +4599,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
[[package]]
name = "windows-result"
version = "0.4.1"
@@ -4013,6 +4687,15 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@@ -4155,7 +4838,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
- "indexmap",
+ "indexmap 2.13.0",
"prettyplease",
"syn 2.0.117",
"wasm-metadata",
@@ -4185,8 +4868,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
- "bitflags",
- "indexmap",
+ "bitflags 2.11.0",
+ "indexmap 2.13.0",
"log",
"serde",
"serde_derive",
@@ -4205,7 +4888,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
- "indexmap",
+ "indexmap 2.13.0",
"log",
"semver",
"serde",
@@ -4352,7 +5035,7 @@ dependencies = [
"arbitrary",
"crc32fast",
"flate2",
- "indexmap",
+ "indexmap 2.13.0",
"memchr",
"zopfli",
]
diff --git a/Cargo.toml b/Cargo.toml
index 19e6efdab..52a1d6b8f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,9 @@ duckdb-prebuilt = []
openhands-subscription-credentials = []
[dependencies]
+agent-client-protocol = "=2.2.0"
+agent-client-protocol-schema = "=1.9.1"
+aho-corasick = "=1.1.4"
async-stream.workspace = true
async-trait.workspace = true
axum.workspace = true
@@ -68,16 +71,23 @@ uuid.workspace = true
[target.'cfg(unix)'.dependencies]
rustix = { workspace = true, features = ["fs"] }
+[target.'cfg(windows)'.dependencies]
+process-wrap = { version = "=10.0.0", default-features = false, features = ["tokio1", "job-object", "kill-on-drop"] }
+windows-sys = { version = "=0.61.2", features = ["Wdk_Storage_FileSystem", "Win32_Storage_FileSystem", "Win32_System_IO"] }
+
+[dev-dependencies]
+tokio = { workspace = true, features = ["test-util"] }
+
[workspace.package]
edition = "2024"
license = "MIT"
repository = "https://github.com/kumanday/OpenSymphony"
rust-version = "1.97.1"
-version = "2.11.3"
+version = "3.0.0"
[workspace.lints.rust]
rust_2018_idioms = { level = "warn", priority = -1 }
-unsafe_code = "forbid"
+unsafe_code = "deny"
unused_lifetimes = "warn"
unused_macro_rules = "warn"
unused_qualifications = "warn"
@@ -117,7 +127,7 @@ thiserror = "2"
time = "=0.3.47"
tokio = { version = "1", features = ["fs", "io-std", "io-util", "macros", "net", "process", "rt-multi-thread", "signal", "sync", "time"] }
tokio-tungstenite = { version = "0.28", features = ["rustls-tls-native-roots"] }
-tokio-util = { version = "0.7", features = ["io"] }
+tokio-util = { version = "0.7", features = ["io", "codec"] }
toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
diff --git a/README.md b/README.md
index a9bbfd405..9a81de415 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# OpenSymphony
-OpenSymphony is a Rust implementation of the [OpenAI Symphony](https://github.com/openai/symphony) specification for orchestrating AI coding agents. It connects to [Linear](https://linear.app) for issue tracking and can run issues through either the managed [OpenHands](https://github.com/OpenHands/OpenHands) agent-server or the local Codex app-server harness.
+OpenSymphony is a Rust implementation of the [OpenAI Symphony](https://github.com/openai/symphony) specification for orchestrating AI coding agents. It connects to [Linear](https://linear.app) for issue tracking and runs issues through the managed [OpenHands](https://github.com/OpenHands/OpenHands) agent-server, the local Codex app-server, or a local ACP v1 agent.

@@ -10,18 +10,19 @@ OpenSymphony automates software development workflows by:
1. **Polling Linear** for issues in active states (Todo, In Progress, etc.)
2. **Creating isolated workspaces** for each issue with lifecycle hooks
-3. **Dispatching AI agents** via OpenHands or Codex to work on issues autonomously
+3. **Dispatching AI agents** through OpenHands, Codex, or a configured ACP profile
4. **Managing retries, reconciliation, and cleanup** based on issue state changes
5. **Providing a desktop operator dashboard** for task graph, run detail, and memory navigation
### Key Features
- **Hierarchy-aware scheduling**: Parent issues wait for sub-issues to complete
-- **WebSocket-first runtime**: Real-time agent updates with REST reconciliation
+- **Multi-repository projects**: Repository-bound sub-issues run in their own checkouts; an unlabeled parent checks their merged work
+- **Live runtime updates**: OpenHands uses WebSocket with REST reconciliation; Codex and ACP use local stdio
- **Per-issue workspaces**: Deterministic, isolated directories with lifecycle hooks
-- **GraphQL-only Linear integration**: Agent-side Linear reads and writes through checked-in helper/query assets
+- **GraphQL-only Linear integration**: The scheduler polls Linear; agent-side writes use checked-in helper/query assets
- **Dependency-aware Task Graph**: Shows dispatchable work, roadmap backlog, and selected-task critical paths
-- **Harness selection**: Default OpenHands agent-server execution, plus local Codex app-server support for ChatGPT subscription-backed runs
+- **Harness selection**: OpenHands, local Codex app-server, or a configured ACP v1 stdio agent
- **Code Graph**: Tree-sitter-backed symbols, diagnostics, and source-cited structural context for agents
- **Operational Knowledge Graph**: Builds agent-queriable and human-navigable memory from completed work
@@ -29,6 +30,11 @@ OpenSymphony `1.0.0` is the compatibility boundary for the GraphQL-only Linear
rewrite. See [Migration Guide](docs/migration-1.0.0.md) if you are upgrading an
older setup.
+Version 3.0.0 adds explicit multi-repository project routing and production
+ACP worker profiles. Existing single-repository workflows remain available.
+See the [3.0 upgrade guide](docs/migration-3.0.0.md) before selecting a new
+routing mode or harness.
+
Packaging note: crates.io exposes a single public package, `opensymphony`.
Internally, the repo still keeps clear subsystem boundaries under
`crates/opensymphony-*`, but those directories are now internal module trees,
@@ -216,6 +222,15 @@ cd /path/to/target-repo
opensymphony run
```
+For a central configuration in another location, pass
+`--config /absolute/path/to/config.yaml` to `opensymphony run`.
+The default single-repository workflow needs no multi-repository labels.
+For a project spanning several repositories, follow the
+[multi-repository guide](docs/multi-repository.md). To run a local ACP agent,
+follow the [ACP harness guide](docs/acp.md). Repository routing and harness
+selection are independent: an ACP profile can run a child bound to one
+repository in a multi-repository project.
+
For real-time monitoring while the orchestrator is running, run the TUI in a separate terminal window:
```bash
opensymphony tui
@@ -335,10 +350,11 @@ flowchart TB
orchestrator["Orchestrator Scheduler"]
workspace["Workspace Manager"]
control["Gateway + Control API
GET /healthz, /api/v1/snapshot, /api/v1/capabilities"]
- runtime["Harness Runtime Client
OpenHands REST/WebSocket or Codex stdio"]
+ runtime["Harness Runtime Client"]
linear_read["Linear Read Adapter"]
+ routing["Repository binding and parent integration"]
- orchestrator --> workspace
+ orchestrator --> routing --> workspace
orchestrator --> runtime
orchestrator --> linear_read
orchestrator --> control
@@ -357,13 +373,19 @@ flowchart TB
linear["Linear"]
openhands["OpenHands Agent-Server"]
codex["Codex App-Server"]
+ acp["Local ACP v1 agent"]
+ config["Central project set and harness profiles"]
operator --> control
+ config --> routing
+ config --> runtime
workspace --> issue_ws
runtime --> openhands
runtime --> codex
+ runtime --> acp
openhands --> agent
codex --> agent
+ acp --> agent
linear_read -->|read issues| linear
graphql -->|agent-side writes| linear
```
@@ -517,11 +539,14 @@ OPENSYMPHONY_LIVE_OPENHANDS=1 ./scripts/live_e2e.sh
- [Architecture](docs/architecture.md) - High-level design and component interactions
- [Configuration](docs/configuration.md) - Target repo bootstrap and runtime config
+- [Multi-repository projects](docs/multi-repository.md) - Repository labels, parent integration, and rollout
+- [ACP harness](docs/acp.md) - Local agent profiles, operator requests, and recovery
- [Deployment Modes](docs/deployment-modes.md) - Local vs hosted deployment
- [Installer and Distribution Strategy](docs/installer-and-distribution.md) - Future signed installer shape and DuckDB packaging boundaries
- [Operations](docs/operations.md) - Doctor, rehydration, diagnostics, and local ops
- [Testing](docs/testing-and-operations.md) - Test strategy and validation layers
- [Migration Guide](docs/migration-1.0.0.md) - Breaking changes and upgrade steps for 1.0.0
+- [3.0 upgrade guide](docs/migration-3.0.0.md) - Moving from single-repository workflows and selecting ACP
- [AGENTS.md](AGENTS.md) - Repository guidelines for coding agents
- [Development Guide](docs/DEVELOPMENT.md) - Contributing and development details
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index c27a7aaf2..96de2095a 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,6 +1,6 @@
{
"name": "@opensymphony/desktop",
- "version": "2.11.3",
+ "version": "3.0.0",
"description": "OpenSymphony Tauri desktop app shell",
"type": "module",
"private": true,
diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock
index f62ccc9ff..a525103c5 100644
--- a/apps/desktop/src-tauri/Cargo.lock
+++ b/apps/desktop/src-tauri/Cargo.lock
@@ -2501,7 +2501,7 @@ dependencies = [
[[package]]
name = "opensymphony-desktop"
-version = "2.11.3"
+version = "3.0.0"
dependencies = [
"chrono",
"directories",
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index 112985ebd..1b81df017 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "opensymphony-desktop"
-version = "2.11.3"
+version = "3.0.0"
description = "OpenSymphony Tauri desktop application shell"
license = "MIT"
homepage = "https://opensymphony.dev"
diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs
index 24bc6c189..f2b9e5786 100644
--- a/apps/desktop/src-tauri/src/commands.rs
+++ b/apps/desktop/src-tauri/src/commands.rs
@@ -825,7 +825,7 @@ async fn update_gateway_connection(
}
async fn set_gateway_connected_for_url(
- state: &tauri::State<'_, RwLock>,
+ state: &RwLock,
base_url: &str,
connected: bool,
) {
@@ -884,6 +884,14 @@ async fn gateway_request_json(
state: tauri::State<'_, RwLock>,
method: reqwest::Method,
path: &str,
+) -> CommandResult {
+ gateway_request_json_from_connection(&state, method, path).await
+}
+
+async fn gateway_request_json_from_connection(
+ state: &RwLock,
+ method: reqwest::Method,
+ path: &str,
) -> CommandResult {
let (base_url, auth_token, client) = {
let conn = state.read().await;
@@ -1379,6 +1387,19 @@ pub async fn run_approvals(
.await
}
+/// Get structured operator input requests.
+#[command]
+pub async fn run_inputs(
+ state: tauri::State<'_, RwLock>,
+ run_id: String,
+) -> CommandResult {
+ gateway_get_json(
+ state,
+ &format!("/api/v1/runs/{}/inputs", urlencoding::encode(&run_id)),
+ )
+ .await
+}
+
/// Get run events with cursor support.
#[command]
pub async fn run_events(
@@ -1477,12 +1498,7 @@ fn profile_is_current_gateway_available(
// βββ Gateway Local Stream Transport (COE-410) ββββββββββββββββββββββββββββββ
use crate::opensymphony_gateway_schema::{
- capability::{
- AuthMode, FeatureCapability as GatewayFeatureCapability, GatewayCapabilities,
- HarnessCapability, TransportCapability as GatewayTransportCapability,
- },
- envelope::GatewayEnvelope,
- version::SchemaVersion,
+ capability::GatewayCapabilities, envelope::GatewayEnvelope,
};
/// Request to subscribe to the gateway event stream via Tauri channel.
@@ -1525,48 +1541,20 @@ pub struct GatewayConnectionInfo {
/// Query gateway capabilities.
/// Used by the frontend transport factory to select the optimal profile.
#[command]
-pub async fn gateway_capabilities() -> CommandResult {
- // Desktop exposes the same loopback gateway capability truth consumed by
- // the frontend. Harness execution still routes through the gateway/runtime,
- // not a separate desktop-side Codex launcher.
- Ok(GatewayCapabilities {
- schema_version: SchemaVersion::v1(),
- gateway_version: env!("CARGO_PKG_VERSION").to_string(),
- supported_api_versions: vec!["1.0.0".to_string()],
- transports: vec![GatewayTransportCapability {
- transport: "loopback_http".to_string(),
- modes: vec!["json".to_string()],
- supported_encodings: vec!["utf-8".to_string()],
- bidirectional: false,
- }],
- harnesses: vec![
- HarnessCapability::openhands_agent_server(),
- HarnessCapability::codex_app_server_local(),
- HarnessCapability::rust_native_future(),
- ],
- features: vec![
- GatewayFeatureCapability {
- feature: "task_graph".to_string(),
- available: true,
- requires_auth: false,
- requires_plan: None,
- },
- GatewayFeatureCapability {
- feature: "terminal_stream".to_string(),
- available: false,
- requires_auth: false,
- requires_plan: None,
- },
- GatewayFeatureCapability {
- feature: "tauri_channel".to_string(),
- available: false,
- requires_auth: false,
- requires_plan: None,
- },
- ],
- auth_modes: vec![AuthMode::None, AuthMode::ApiKey],
- max_event_page_size: 1000,
- max_terminal_frame_batch: 500,
+pub async fn gateway_capabilities(
+ state: tauri::State<'_, RwLock>,
+) -> CommandResult {
+ gateway_capabilities_from_connection(&state).await
+}
+
+async fn gateway_capabilities_from_connection(
+ state: &RwLock,
+) -> CommandResult {
+ let value =
+ gateway_request_json_from_connection(state, reqwest::Method::GET, "/api/v1/capabilities")
+ .await?;
+ serde_json::from_value(value).map_err(|error| DesktopError::Gateway {
+ message: format!("Invalid gateway capabilities: {error}"),
})
}
@@ -1767,30 +1755,68 @@ mod tests {
}
#[tokio::test]
- async fn gateway_capabilities_advertises_http_only_native_transport() {
- let capabilities = gateway_capabilities().await.unwrap();
- let transports: Vec<&str> = capabilities
- .transports
- .iter()
- .map(|transport| transport.transport.as_str())
- .collect();
-
- assert_eq!(transports, vec!["loopback_http"]);
-
- let harnesses: Vec<(&str, bool)> = capabilities
- .harnesses
- .iter()
- .map(|harness| (harness.kind.as_str(), harness.available))
- .collect();
-
- assert_eq!(
- harnesses,
- vec![
- ("openhands_agent_server", true),
- ("codex_app_server", true),
- ("rust_native", false),
- ]
- );
+ async fn gateway_capabilities_reflects_attached_gateway_acp_profile() {
+ use crate::opensymphony_gateway_schema::{
+ capability::{
+ AuthMode, HarnessCapability, HarnessProfileCapability, TransportCapability,
+ },
+ version::SchemaVersion,
+ };
+ use std::io::{BufRead, BufReader, Write};
+
+ let advertised = GatewayCapabilities {
+ schema_version: SchemaVersion::v1(),
+ gateway_version: "configured-gateway".into(),
+ supported_api_versions: vec!["1.0.0".into()],
+ transports: vec![TransportCapability {
+ transport: "loopback_http".into(),
+ modes: vec!["json".into()],
+ supported_encodings: vec!["utf-8".into()],
+ bidirectional: false,
+ }],
+ harnesses: vec![HarnessCapability::acp()],
+ harness_profiles: vec![HarnessProfileCapability {
+ harness: "acp".into(),
+ profile_id: "local-acp".into(),
+ preflight_ready: true,
+ unavailable_reason: None,
+ }],
+ features: Vec::new(),
+ auth_modes: vec![AuthMode::None],
+ max_event_page_size: 1000,
+ max_terminal_frame_batch: 500,
+ };
+ let body = serde_json::to_string(&advertised).unwrap();
+ let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let base_url = format!("http://{}", listener.local_addr().unwrap());
+ let server = std::thread::spawn(move || {
+ let (mut stream, _) = listener.accept().unwrap();
+ let mut reader = BufReader::new(stream.try_clone().unwrap());
+ let mut request = String::new();
+ loop {
+ let mut line = String::new();
+ reader.read_line(&mut line).unwrap();
+ assert!(!line.is_empty(), "gateway request closed before headers");
+ if line == "\r\n" {
+ break;
+ }
+ request.push_str(&line);
+ }
+ assert!(request.starts_with("GET /api/v1/capabilities "));
+ assert!(request
+ .to_ascii_lowercase()
+ .contains("authorization: bearer test-token"));
+ write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).unwrap();
+ });
+ let state = RwLock::new(GatewayConnection {
+ base_url,
+ auth_token: Some("test-token".into()),
+ ..GatewayConnection::default()
+ });
+ let actual = gateway_capabilities_from_connection(&state).await.unwrap();
+ server.join().unwrap();
+ assert_eq!(actual, advertised);
+ assert!(state.read().await.connected);
}
#[test]
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index 9c8aa68c0..fe1a127eb 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -82,6 +82,7 @@ pub fn run() {
commands::run_diffs,
commands::run_validation,
commands::run_approvals,
+ commands::run_inputs,
commands::run_events,
commands::terminal_snapshot,
commands::get_connection_profiles,
diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json
index 2a3b03542..45cb9cf91 100644
--- a/apps/desktop/src-tauri/tauri.conf.json
+++ b/apps/desktop/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"productName": "OpenSymphony",
"mainBinaryName": "OpenSymphony",
- "version": "2.11.3",
+ "version": "3.0.0",
"identifier": "dev.opensymphony.app",
"build": {
"beforeDevCommand": "npm run dev",
diff --git a/apps/desktop/src/index.ts b/apps/desktop/src/index.ts
index 303b1c799..e58f71a3d 100644
--- a/apps/desktop/src/index.ts
+++ b/apps/desktop/src/index.ts
@@ -310,6 +310,12 @@ class DesktopTransportAdapter implements TauriTransportAdapter {
).then((response) => response.approvals ?? []);
}
+ runInputs(runId: string): ReturnType {
+ return this.invokeOrHttp<{ inputs?: Awaited> }>(
+ "run_inputs", { runId }, async () => ({ inputs: await this.inner.runInputs(runId) }),
+ ).then((response) => response.inputs ?? []);
+ }
+
runValidation(runId: string): ReturnType {
return this.invokeOrHttp("run_validation", { runId }, () => this.inner.runValidation(runId));
}
@@ -334,20 +340,28 @@ class DesktopTransportAdapter implements TauriTransportAdapter {
return this.actionInner.dispatchAction(action);
}
- cancelRun(runId: string): Promise {
- return this.actionInner.cancelRun(runId);
+ cancelRun(runId: string, operationId?: string): Promise {
+ return this.actionInner.cancelRun(runId, operationId);
+ }
+
+ replanParent(
+ parentId: string,
+ hierarchyGeneration: number,
+ operationId?: string,
+ ): Promise {
+ return this.actionInner.replanParent(parentId, hierarchyGeneration, operationId);
}
- retryRun(runId: string): Promise {
- return this.actionInner.retryRun(runId);
+ retryRun(runId: string, operationId?: string): Promise {
+ return this.actionInner.retryRun(runId, operationId);
}
- resumeRun(runId: string): Promise {
- return this.actionInner.resumeRun(runId);
+ resumeRun(runId: string, operationId?: string): Promise {
+ return this.actionInner.resumeRun(runId, operationId);
}
- rehydrateRun(runId: string): Promise {
- return this.actionInner.rehydrateRun(runId);
+ rehydrateRun(runId: string, operationId?: string): Promise {
+ return this.actionInner.rehydrateRun(runId, operationId);
}
commentRun(runId: string, text: string): Promise {
@@ -362,8 +376,10 @@ class DesktopTransportAdapter implements TauriTransportAdapter {
approvalId: string,
decision: "approved" | "rejected",
explanation?: string,
+ interaction?: import("@opensymphony/gateway-schema").OperatorInteraction,
+ optionId?: string,
): Promise {
- return this.actionInner.approvalDecision(approvalId, decision, explanation);
+ return this.actionInner.approvalDecision(approvalId, decision, explanation, interaction, optionId);
}
openWorkspace(runId: string): Promise {
diff --git a/apps/web/src/index.ts b/apps/web/src/index.ts
index 9f7530803..19f143ff8 100644
--- a/apps/web/src/index.ts
+++ b/apps/web/src/index.ts
@@ -87,6 +87,10 @@ class BrowserTransport implements BrowserTransportAdapter {
return this.inner.runApprovals(runId);
}
+ runInputs(runId: string): ReturnType {
+ return this.inner.runInputs(runId);
+ }
+
runValidation(runId: string): ReturnType {
return this.inner.runValidation(runId);
}
@@ -111,20 +115,28 @@ class BrowserTransport implements BrowserTransportAdapter {
return this.inner.dispatchAction(action);
}
- cancelRun(runId: string): Promise {
- return this.inner.cancelRun(runId);
+ cancelRun(runId: string, operationId?: string): Promise {
+ return this.inner.cancelRun(runId, operationId);
+ }
+
+ replanParent(
+ parentId: string,
+ hierarchyGeneration: number,
+ operationId?: string,
+ ): Promise {
+ return this.inner.replanParent(parentId, hierarchyGeneration, operationId);
}
- retryRun(runId: string): Promise {
- return this.inner.retryRun(runId);
+ retryRun(runId: string, operationId?: string): Promise {
+ return this.inner.retryRun(runId, operationId);
}
- resumeRun(runId: string): Promise {
- return this.inner.resumeRun(runId);
+ resumeRun(runId: string, operationId?: string): Promise {
+ return this.inner.resumeRun(runId, operationId);
}
- rehydrateRun(runId: string): Promise {
- return this.inner.rehydrateRun(runId);
+ rehydrateRun(runId: string, operationId?: string): Promise {
+ return this.inner.rehydrateRun(runId, operationId);
}
commentRun(runId: string, text: string): Promise {
@@ -139,8 +151,10 @@ class BrowserTransport implements BrowserTransportAdapter {
approvalId: string,
decision: "approved" | "rejected",
explanation?: string,
+ interaction?: import("@opensymphony/gateway-schema").OperatorInteraction,
+ optionId?: string,
): Promise {
- return this.inner.approvalDecision(approvalId, decision, explanation);
+ return this.inner.approvalDecision(approvalId, decision, explanation, interaction, optionId);
}
openWorkspace(runId: string): Promise {
diff --git a/crates/opensymphony-acp/src/atomic_file.rs b/crates/opensymphony-acp/src/atomic_file.rs
new file mode 100644
index 000000000..fda73949a
--- /dev/null
+++ b/crates/opensymphony-acp/src/atomic_file.rs
@@ -0,0 +1,371 @@
+//! Cancel-safe callback writes: only a completed stage may replace the destination.
+use std::{io, path::Path};
+use tokio::fs::File;
+#[cfg(windows)]
+#[path = "windows_atomic.rs"]
+mod windows_atomic;
+
+pub(super) struct AtomicFile {
+ pub file: File,
+ #[cfg(unix)]
+ directory: std::os::fd::OwnedFd,
+ #[cfg(unix)]
+ temporary: std::ffi::OsString,
+ #[cfg(unix)]
+ destination: std::ffi::OsString,
+ #[cfg(not(unix))]
+ temporary: Option,
+ #[cfg(not(unix))]
+ destination: std::path::PathBuf,
+ #[cfg(windows)]
+ _guards: Vec,
+}
+impl AtomicFile {
+ #[cfg(any(not(unix), test))]
+ pub async fn new(root: &Path, path: &Path) -> io::Result {
+ #[cfg(unix)]
+ {
+ use rustix::fs::{Mode, OFlags, open};
+ let root_directory = open(
+ root,
+ OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
+ Mode::empty(),
+ )?;
+ Self::new_pinned(root, path, &root_directory).await
+ }
+ #[cfg(not(unix))]
+ {
+ let _ = root;
+ let parent = path.parent().ok_or(io::ErrorKind::InvalidInput)?;
+ #[cfg(windows)]
+ let guards = super::windows_path::pin_directory(parent, true).await?;
+ #[cfg(not(windows))]
+ tokio::fs::create_dir_all(parent).await?;
+ let permissions = match tokio::fs::symlink_metadata(path).await {
+ Ok(metadata) => {
+ #[cfg(windows)]
+ {
+ use std::os::windows::fs::MetadataExt;
+ if metadata.file_attributes() & 0x400 != 0 {
+ return Err(io::ErrorKind::InvalidInput.into());
+ }
+ }
+ if !metadata.is_file() || metadata.permissions().readonly() {
+ return Err(io::ErrorKind::InvalidInput.into());
+ }
+ Some(metadata.permissions())
+ }
+ Err(error) if error.kind() == io::ErrorKind::NotFound => None,
+ Err(error) => return Err(error),
+ };
+ #[cfg(not(windows))]
+ let temporary = tempfile::Builder::new()
+ .prefix(".opensymphony-write-")
+ .tempfile_in(parent)?;
+ #[cfg(windows)]
+ let temporary = tempfile::Builder::new()
+ .prefix(".opensymphony-write-")
+ .make_in(parent, windows_atomic::create_stage)?;
+ if let Some(permissions) = permissions {
+ temporary.as_file().set_permissions(permissions)?;
+ }
+ let file = File::from_std(temporary.as_file().try_clone()?);
+ Ok(Self {
+ file,
+ temporary: Some(temporary),
+ destination: path.into(),
+ #[cfg(windows)]
+ _guards: guards,
+ })
+ }
+ }
+
+ #[cfg(unix)]
+ pub async fn new_pinned(
+ root: &Path,
+ path: &Path,
+ root_directory: &std::os::fd::OwnedFd,
+ ) -> io::Result {
+ use rustix::fs::{FileType, Mode, OFlags, fchmod, fstat, mkdirat, openat};
+ let relative = path
+ .strip_prefix(root)
+ .map_err(|_| io::ErrorKind::InvalidInput)?;
+ let destination = relative
+ .file_name()
+ .ok_or(io::ErrorKind::InvalidInput)?
+ .to_owned();
+ let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
+ let mut directory = openat(root_directory, ".", flags, Mode::empty())?;
+ for component in relative
+ .parent()
+ .ok_or(io::ErrorKind::InvalidInput)?
+ .components()
+ {
+ if !matches!(component, std::path::Component::Normal(_)) {
+ return Err(io::ErrorKind::InvalidInput.into());
+ }
+ match mkdirat(
+ &directory,
+ component.as_os_str(),
+ Mode::from_raw_mode(0o700),
+ ) {
+ Ok(()) | Err(rustix::io::Errno::EXIST) => {}
+ Err(error) => return Err(error.into()),
+ }
+ directory = openat(&directory, component.as_os_str(), flags, Mode::empty())?;
+ }
+ // Preserve the original file's write authorization and permissions,
+ // without truncating it or following a substituted leaf symlink.
+ let permissions = match openat(
+ &directory,
+ &destination,
+ OFlags::WRONLY | OFlags::NONBLOCK | OFlags::NOFOLLOW | OFlags::CLOEXEC,
+ Mode::empty(),
+ ) {
+ Ok(file) => {
+ let metadata = fstat(&file)?;
+ if FileType::from_raw_mode(metadata.st_mode) != FileType::RegularFile {
+ return Err(io::ErrorKind::InvalidInput.into());
+ }
+ Some(Mode::from_raw_mode(metadata.st_mode))
+ }
+ Err(rustix::io::Errno::NOENT) => None,
+ Err(error) => return Err(error.into()),
+ };
+ let temporary =
+ std::ffi::OsString::from(format!(".opensymphony-write-{}", uuid::Uuid::new_v4()));
+ let file = openat(
+ &directory,
+ &temporary,
+ OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC,
+ Mode::from_raw_mode(0o600),
+ )?;
+ let result = Self {
+ file: File::from_std(std::fs::File::from(file)),
+ directory,
+ temporary,
+ destination,
+ };
+ if let Some(permissions) = permissions {
+ fchmod(&result.file, permissions)?;
+ }
+ Ok(result)
+ }
+
+ // No await between the caller's last cancellation check and atomic replace.
+ pub fn commit(mut self) -> io::Result<()> {
+ #[cfg(unix)]
+ {
+ rustix::fs::renameat(
+ &self.directory,
+ &self.temporary,
+ &self.directory,
+ &self.destination,
+ )?;
+ self.temporary.clear();
+ }
+ #[cfg(windows)]
+ {
+ let temporary = self.temporary.as_mut().ok_or(io::ErrorKind::InvalidInput)?;
+ windows_atomic::replace_in_same_directory(
+ temporary.as_file(),
+ self.destination
+ .file_name()
+ .ok_or(io::ErrorKind::InvalidInput)?,
+ )?;
+ // The original name no longer refers to our stage. Do not attempt
+ // path cleanup, which could remove another file created there.
+ temporary.disable_cleanup(true);
+ self.temporary.take();
+ }
+ #[cfg(not(any(unix, windows)))]
+ {
+ // Other platforms use the tempfile crate's atomic promotion.
+ drop(self.file);
+ self.temporary
+ .take()
+ .ok_or(io::ErrorKind::InvalidInput)?
+ .into_temp_path()
+ .persist(&self.destination)
+ .map_err(|error| error.error)?;
+ }
+ Ok(())
+ }
+}
+#[cfg(windows)]
+impl Drop for AtomicFile {
+ fn drop(&mut self) {
+ if let Some(temporary) = self.temporary.as_mut() {
+ // Cancellation may leave a Tokio blocking write holding a cloned
+ // handle. Deletion by handle completes when its last handle closes.
+ if windows_atomic::delete_on_close(temporary.as_file()).is_ok() {
+ temporary.disable_cleanup(true);
+ }
+ }
+ }
+}
+#[cfg(unix)]
+impl Drop for AtomicFile {
+ fn drop(&mut self) {
+ if !self.temporary.is_empty() {
+ let _ = rustix::fs::unlinkat(
+ &self.directory,
+ &self.temporary,
+ rustix::fs::AtFlags::empty(),
+ );
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tokio::io::AsyncWriteExt;
+
+ #[tokio::test]
+ async fn partial_stage_drop_preserves_original_and_completed_stage_replaces_it() {
+ let root = tempfile::tempdir().expect("root");
+ let root = root.path().canonicalize().expect("canonical");
+ let path = root.join("file");
+ std::fs::write(&path, "original").expect("original");
+ let mut stage = AtomicFile::new(&root, &path).await.expect("stage");
+ stage
+ .file
+ .write_all(b"partial")
+ .await
+ .expect("partial write");
+ stage.file.flush().await.expect("flush partial");
+ assert_eq!(
+ std::fs::read(&path).expect("original survives staging"),
+ b"original"
+ );
+ drop(stage);
+ assert_eq!(std::fs::read_dir(&root).expect("entries").count(), 1);
+ assert_eq!(
+ std::fs::read(&path).expect("cancel leaves original"),
+ b"original"
+ );
+ let mut stage = AtomicFile::new(&root, &path).await.expect("stage");
+ stage.file.write_all(b"complete").await.expect("write");
+ stage.file.flush().await.expect("flush");
+ stage.commit().expect("commit");
+ assert_eq!(std::fs::read(&path).expect("replaced"), b"complete");
+ assert_eq!(std::fs::read_dir(&root).expect("entries").count(), 1);
+ }
+
+ #[tokio::test]
+ async fn stage_io_error_preserves_original_and_removes_temporary_file() {
+ let root = tempfile::tempdir().expect("root");
+ let root = root.path().canonicalize().expect("canonical");
+ let path = root.join("file");
+ std::fs::write(&path, "original").expect("original");
+ let mut stage = AtomicFile::new(&root, &path).await.expect("stage");
+ #[cfg(unix)]
+ let temporary_path = root.join(&stage.temporary);
+ #[cfg(not(unix))]
+ let temporary_path = stage.temporary.as_ref().expect("stage").path().to_owned();
+ // Reopen the actual stage read-only to deterministically inject a write
+ // failure at the same I/O boundary used by the callback.
+ stage.file = File::from_std(std::fs::File::open(&temporary_path).expect("read-only stage"));
+ assert!(
+ stage.file.write_all(b"replacement").await.is_err()
+ || stage.file.flush().await.is_err()
+ );
+ drop(stage);
+ assert_eq!(std::fs::read(&path).expect("unchanged"), b"original");
+ assert_eq!(std::fs::read_dir(&root).expect("entries").count(), 1);
+ }
+
+ #[cfg(windows)]
+ #[tokio::test]
+ async fn windows_promotion_pins_stage_and_parent_and_preserves_original_on_error() {
+ let root = tempfile::tempdir().expect("root");
+ let root = root.path().canonicalize().expect("canonical");
+ let inside = root.join("inside");
+ std::fs::create_dir(&inside).expect("directory");
+ let path = inside.join("file");
+ std::fs::write(&path, "original").expect("original");
+ let mut stage = AtomicFile::new(&root, &path).await.expect("stage");
+ let temporary = stage.temporary.as_ref().expect("stage").path();
+ assert!(std::fs::rename(temporary, root.join("escaped-stage")).is_err());
+ assert!(std::fs::rename(&inside, root.join("moved")).is_err());
+ stage.file.write_all(b"complete").await.expect("write");
+ stage.file.flush().await.expect("flush");
+ let permissions = std::fs::metadata(&path).expect("metadata").permissions();
+ let mut readonly = permissions.clone();
+ readonly.set_readonly(true);
+ std::fs::set_permissions(&path, readonly).expect("readonly");
+ assert!(stage.commit().is_err(), "readonly destination must survive");
+ assert_eq!(std::fs::read(&path).expect("original"), b"original");
+ assert_eq!(std::fs::read_dir(&inside).expect("entries").count(), 1);
+ assert!(
+ std::fs::metadata(&path)
+ .expect("metadata")
+ .permissions()
+ .readonly()
+ );
+ std::fs::set_permissions(&path, permissions).expect("restore for cleanup");
+ }
+
+ #[cfg(windows)]
+ #[tokio::test]
+ async fn windows_cancel_defers_stage_deletion_until_outstanding_write_handle_closes() {
+ let root = tempfile::tempdir().expect("root");
+ let root = root.path().canonicalize().expect("canonical");
+ let path = root.join("file");
+ std::fs::write(&path, "original").expect("original");
+ let stage = AtomicFile::new(&root, &path).await.expect("stage");
+ // Tokio's blocking write owns a clone of this file object after its
+ // future is cancelled. Keep the same ownership alive deterministically.
+ let mut inflight = stage
+ .temporary
+ .as_ref()
+ .expect("stage")
+ .as_file()
+ .try_clone()
+ .expect("in-flight handle");
+ drop(stage);
+ std::io::Write::write_all(&mut inflight, b"late staged output").expect("late write");
+ drop(inflight);
+ assert_eq!(std::fs::read(&path).expect("original"), b"original");
+ assert_eq!(std::fs::read_dir(&root).expect("entries").count(), 1);
+ }
+
+ #[cfg(unix)]
+ #[tokio::test]
+ async fn atomic_write_keeps_permissions_and_descriptor_relative_parent() {
+ use std::os::unix::fs::PermissionsExt;
+ let root = tempfile::tempdir().expect("root");
+ let root = root.path().canonicalize().expect("canonical");
+ let inside = root.join("inside");
+ std::fs::create_dir(&inside).expect("inside");
+ let path = inside.join("file");
+ std::fs::write(&path, "original").expect("original");
+ std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o640)).expect("mode");
+ let mut stage = AtomicFile::new(&root, &path).await.expect("stage");
+ let outside = tempfile::tempdir().expect("outside");
+ std::fs::write(outside.path().join("file"), "outside").expect("outside");
+ std::fs::rename(&inside, root.join("moved")).expect("rename");
+ std::os::unix::fs::symlink(outside.path(), &inside).expect("swap");
+ stage.file.write_all(b"complete").await.expect("write");
+ stage.file.flush().await.expect("flush");
+ stage.commit().expect("commit into held parent");
+ assert_eq!(
+ std::fs::read(root.join("moved/file")).expect("file"),
+ b"complete"
+ );
+ assert_eq!(
+ std::fs::metadata(root.join("moved/file"))
+ .expect("mode")
+ .permissions()
+ .mode()
+ & 0o777,
+ 0o640
+ );
+ assert_eq!(
+ std::fs::read(outside.path().join("file")).expect("outside"),
+ b"outside"
+ );
+ assert!(AtomicFile::new(&root, &path).await.is_err());
+ }
+}
diff --git a/crates/opensymphony-acp/src/durable.rs b/crates/opensymphony-acp/src/durable.rs
new file mode 100644
index 000000000..08c21e904
--- /dev/null
+++ b/crates/opensymphony-acp/src/durable.rs
@@ -0,0 +1,1187 @@
+//! One durable conversation record and one stable cross-process owner lock.
+use std::{fs::File, io, path::Path};
+
+use serde_json::Value;
+use sha2::{Digest, Sha256};
+
+use super::AcpProfile;
+use crate::opensymphony_workspace::{
+ AcpProcessState, AcpRecovery, AcpSessionIdentity, AcpSessionState, AcpSessionStatus,
+ ConversationManifest, WorkspaceError, WorkspaceHandle, WorkspaceManager,
+};
+
+const MAX_MANIFEST_BYTES: usize = 2 * 1024 * 1024;
+const MAX_ID_BYTES: usize = 4096;
+
+#[derive(Debug, thiserror::Error)]
+pub(super) enum DurabilityError {
+ #[error("ACP session already has a live owner")]
+ Owned,
+ #[error("ACP prior prompt outcome is uncertain; resolve execution risk before another attempt")]
+ Uncertain,
+ #[error(
+ "ACP owner was lost during process launch; resolve process ownership before another launch"
+ )]
+ ProcessUncertain,
+ #[error("ACP cannot replace a native conversation manifest")]
+ NativeManifest,
+ #[error("ACP durable session binding mismatch: {0}")]
+ Binding(&'static str),
+ #[error("ACP durable metadata exceeds its bounds or contains an invalid identifier")]
+ InvalidMetadata,
+ #[error("ACP durable filesystem operation failed ({0:?})")]
+ Io(io::ErrorKind),
+ #[error(transparent)]
+ Workspace(#[from] WorkspaceError),
+}
+
+impl From for DurabilityError {
+ fn from(error: io::Error) -> Self {
+ Self::Io(error.kind())
+ }
+}
+
+pub(super) fn profile_fingerprint(
+ profile: &AcpProfile,
+ services: &super::HostServices,
+ limits: &super::ClientLimits,
+) -> Result {
+ // Persist only the digest: resolved MCP grants are part of session reuse
+ // identity but must never enter durable manifests in cleartext.
+ let encoded = serde_json::to_vec(&(
+ profile,
+ services.read_files,
+ services.write_files,
+ services.terminals,
+ &services.mcp_servers,
+ limits,
+ ))
+ .map_err(|_| DurabilityError::InvalidMetadata)?;
+ Ok(format!("{:x}", Sha256::digest(encoded)))
+}
+
+pub(super) struct Durability {
+ manager: WorkspaceManager,
+ workspace: WorkspaceHandle,
+ manifest: ConversationManifest,
+ // Never unlink this file: another process may already be waiting on its inode.
+ _owner_lock: File,
+}
+
+impl Drop for Durability {
+ fn drop(&mut self) {
+ // Release explicitly: a concurrent fork can temporarily inherit an open
+ // descriptor before exec closes it, delaying close-only flock release.
+ let _ = self._owner_lock.unlock();
+ }
+}
+
+impl Durability {
+ pub(super) fn process_is_absent(&self) -> bool {
+ #[cfg(unix)]
+ {
+ verify_prior_process(self.state().process).is_ok()
+ }
+ #[cfg(not(unix))]
+ {
+ // Windows owner-death recovery relies on closing the JobObject;
+ // that assumption does not prove a live owner's failed stop.
+ self.state().process == AcpProcessState::Stopped
+ }
+ }
+
+ /// Zero generation is the trusted host's request to claim the latest record.
+ /// A nonzero generation requires an exact match before reserving its successor.
+ pub(super) async fn open(
+ manager: WorkspaceManager,
+ workspace: WorkspaceHandle,
+ identity: AcpSessionIdentity,
+ ) -> Result {
+ Self::claim(manager, workspace, identity, true, None).await
+ }
+
+ pub(super) async fn open_bound_parent(
+ manager: WorkspaceManager,
+ workspace: WorkspaceHandle,
+ identity: AcpSessionIdentity,
+ expected_session_id: String,
+ ) -> Result {
+ Self::claim(
+ manager,
+ workspace,
+ identity,
+ true,
+ Some(expected_session_id),
+ )
+ .await
+ }
+
+ pub(super) async fn retire_persisted(
+ manager: WorkspaceManager,
+ workspace: WorkspaceHandle,
+ identity: AcpSessionIdentity,
+ ) -> Result<(), DurabilityError> {
+ // The claim verifies both the exclusive owner lock and process absence;
+ // reserve no launch, so a cleanup crash cannot leave LaunchPending behind.
+ let _guard = Self::claim(manager, workspace, identity, false, None).await?;
+ Ok(())
+ }
+
+ async fn claim(
+ manager: WorkspaceManager,
+ workspace: WorkspaceHandle,
+ identity: AcpSessionIdentity,
+ reserve_launch: bool,
+ expected_session_id: Option,
+ ) -> Result {
+ validate_identity(&identity)?;
+ let issue = manager
+ .load_issue_manifest(&workspace)
+ .await?
+ .ok_or(DurabilityError::Binding("issue manifest missing"))?;
+ if issue.issue_id != workspace.issue_id()
+ || issue.identifier != workspace.identifier()
+ || issue.sanitized_workspace_key != workspace.workspace_key()
+ || issue.workspace_path != workspace.workspace_path()
+ || identity.workspace_path != workspace.workspace_path()
+ {
+ return Err(DurabilityError::Binding("workspace"));
+ }
+ if issue
+ .repository_binding
+ .as_ref()
+ .and_then(|binding| binding.resolved_binding())
+ != identity.repository_binding.as_ref()
+ {
+ return Err(DurabilityError::Binding("repository"));
+ }
+ if workspace.checkout_generation() != identity.checkout_generation.as_deref() {
+ return Err(DurabilityError::Binding("checkout generation"));
+ }
+
+ let lock_path = workspace.metadata_dir().join("acp-owner.lock");
+ let lock_path = manager
+ .validate_workspace_owned_path(&workspace, &lock_path)
+ .await?;
+ let owner_lock = File::options()
+ .read(true)
+ .write(true)
+ .create(true)
+ .truncate(false)
+ .open(&lock_path)?;
+ match owner_lock.try_lock() {
+ Ok(()) => {}
+ Err(std::fs::TryLockError::WouldBlock) => return Err(DurabilityError::Owned),
+ Err(std::fs::TryLockError::Error(error)) => return Err(error.into()),
+ }
+ match tokio::fs::symlink_metadata(workspace.conversation_manifest_path()).await {
+ Ok(metadata) if metadata.len() > MAX_MANIFEST_BYTES as u64 => {
+ return Err(DurabilityError::InvalidMetadata);
+ }
+ Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error.into()),
+ _ => {}
+ }
+ let mut manifest = match manager.load_conversation_manifest(&workspace).await? {
+ Some(manifest) => manifest,
+ None => {
+ let mut manifest = ConversationManifest::new(
+ &workspace,
+ "",
+ "acp://stdio",
+ workspace.metadata_dir(),
+ "acp-v1",
+ );
+ manifest.acp = Some(AcpSessionState {
+ harness: "acp".into(),
+ identity: AcpSessionIdentity {
+ generation: 0,
+ ..identity.clone()
+ },
+ session_id: None,
+ initialization: Value::Null,
+ enabled_operations: Vec::new(),
+ model_selection: false,
+ status: AcpSessionStatus::Ready,
+ stop_reason: None,
+ workflow_prompt_seeded: Some(false),
+ recovery: AcpRecovery::Fresh,
+ owner_id: String::new(),
+ process: AcpProcessState::Stopped,
+ });
+ manifest
+ }
+ };
+ if manifest.issue_id != workspace.issue_id()
+ || manifest.identifier != workspace.identifier()
+ {
+ return Err(DurabilityError::Binding("issue"));
+ }
+ let run = if reserve_launch {
+ manager.load_run_manifest(&workspace).await?
+ } else {
+ None
+ };
+ if expected_session_id.is_some() && run.is_none() {
+ return Err(DurabilityError::Binding("parent run"));
+ }
+ if let Some(run) = run {
+ if run.run_id != identity.run_id || run.attempt != identity.attempt {
+ return Err(DurabilityError::Binding("run identity"));
+ }
+ if let Some(expected) = expected_session_id.as_deref()
+ && run
+ .parent_runtime_envelope
+ .as_ref()
+ .and_then(|envelope| envelope.conversation_binding.as_deref())
+ != Some(expected)
+ {
+ return Err(DurabilityError::Binding("parent conversation"));
+ }
+ if let Some(envelope) = run.parent_runtime_envelope {
+ if envelope.harness != "acp" || envelope.workspace_path != identity.workspace_path {
+ return Err(DurabilityError::Binding("parent runtime envelope"));
+ }
+ manifest.parent_runtime_envelope = Some(envelope);
+ }
+ if let Some(envelope) = run.runtime_envelope {
+ if envelope.harness != "acp"
+ || envelope.checkout_path != identity.workspace_path
+ || Some(&envelope.repository_binding) != identity.repository_binding.as_ref()
+ || Some(envelope.checkout_generation.as_str())
+ != identity.checkout_generation.as_deref()
+ {
+ return Err(DurabilityError::Binding("runtime envelope"));
+ }
+ manifest.runtime_envelope = Some(envelope);
+ }
+ }
+ let state = manifest
+ .acp
+ .as_mut()
+ .ok_or(DurabilityError::NativeManifest)?;
+ if let Some(expected) = expected_session_id.as_deref() {
+ if state.session_id.as_deref() != Some(expected)
+ || manifest.conversation_id.as_str() != expected
+ || manifest
+ .parent_runtime_envelope
+ .as_ref()
+ .and_then(|envelope| envelope.conversation_binding.as_deref())
+ != Some(expected)
+ {
+ return Err(DurabilityError::Binding("parent conversation"));
+ }
+ validate_static_binding(&state.identity, &identity)?;
+ } else {
+ validate_binding(&state.identity, &identity)?;
+ }
+ if state.harness != "acp"
+ || manifest.conversation_id != state.session_id.as_deref().unwrap_or_default()
+ {
+ return Err(DurabilityError::Binding("harness/session"));
+ }
+ if matches!(
+ state.status,
+ AcpSessionStatus::Submitted | AcpSessionStatus::Uncertain
+ ) {
+ return Err(DurabilityError::Uncertain);
+ }
+ verify_prior_process(state.process)?;
+ // Migrate before a new run clears the old terminal status and stop reason.
+ if state.workflow_prompt_seeded.is_none() {
+ state.workflow_prompt_seeded = Some(state.workflow_prompt_seeded());
+ }
+ if identity.generation != 0 && identity.generation != state.identity.generation {
+ return Err(DurabilityError::Binding("connection generation"));
+ }
+ if reserve_launch {
+ if expected_session_id.is_some() {
+ // The old owner has stopped and the parent conversation is
+ // bound to this run. Rotate only the process/grant revision;
+ // preserve the session ID for negotiated load/resume.
+ state.identity.profile_fingerprint = identity.profile_fingerprint.clone();
+ state.identity.credential_scope = identity.credential_scope.clone();
+ }
+ let new_run = state.identity.run_id != identity.run_id
+ || state.identity.attempt != identity.attempt;
+ state.identity.generation = state
+ .identity
+ .generation
+ .checked_add(1)
+ .ok_or(DurabilityError::Binding("connection generation exhausted"))?;
+ state.identity.run_id = identity.run_id;
+ state.identity.attempt = identity.attempt;
+ if new_run {
+ state.status = AcpSessionStatus::Ready;
+ state.stop_reason = None;
+ }
+ state.owner_id = uuid::Uuid::new_v4().to_string();
+ state.process = AcpProcessState::LaunchPending;
+ } else {
+ state.process = AcpProcessState::Stopped;
+ }
+ let durable = Self {
+ manager,
+ workspace,
+ manifest,
+ _owner_lock: owner_lock,
+ };
+ // Claim the generation before launching a process or establishing a connection.
+ durable.persist().await?;
+ Ok(durable)
+ }
+
+ pub(super) fn state(&self) -> &AcpSessionState {
+ self.manifest
+ .acp
+ .as_ref()
+ .expect("ACP manifest initialized on open")
+ }
+
+ pub(super) fn state_mut(&mut self) -> &mut AcpSessionState {
+ self.manifest
+ .acp
+ .as_mut()
+ .expect("ACP manifest initialized on open")
+ }
+
+ pub(super) async fn persist(&self) -> Result<(), DurabilityError> {
+ validate_identity(&self.state().identity)?;
+ if self
+ .state()
+ .session_id
+ .as_deref()
+ .is_some_and(|id| !valid_id(id))
+ || serde_json::to_vec_pretty(&self.manifest)
+ .map_err(|_| DurabilityError::InvalidMetadata)?
+ .len()
+ > MAX_MANIFEST_BYTES
+ {
+ return Err(DurabilityError::InvalidMetadata);
+ }
+ let path = self.workspace.conversation_manifest_path();
+ let mut manifest = self.manifest.clone();
+ if let Some(envelope) = &mut manifest.runtime_envelope {
+ envelope.acp_session = Some(self.state().identity.clone());
+ envelope.conversation_binding = self.state().session_id.clone();
+ envelope.run_id.clone_from(&self.state().identity.run_id);
+ envelope.attempt = self.state().identity.attempt;
+ }
+ if let Some(envelope) = &mut manifest.parent_runtime_envelope {
+ envelope.conversation_binding = self.state().session_id.clone();
+ envelope.run_id.clone_from(&self.state().identity.run_id);
+ envelope.attempt = self.state().identity.attempt;
+ }
+ self.manager
+ .write_json_artifact_atomically(&self.workspace, &path, &manifest)
+ .await?;
+ // Atomic replacement alone does not establish the pre-send durability barrier.
+ let file = tokio::fs::OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(&path)
+ .await?;
+ file.sync_all().await?;
+ sync_directory(&self.workspace.metadata_dir()).await?;
+ sync_directory(self.workspace.workspace_path()).await?;
+ Ok(())
+ }
+
+ pub(super) async fn launched(&mut self, pid: u32) -> Result<(), DurabilityError> {
+ if self.state().process != AcpProcessState::LaunchPending || pid == 0 {
+ return Err(DurabilityError::InvalidMetadata);
+ }
+ self.state_mut().process = AcpProcessState::Running { pid };
+ self.persist().await
+ }
+
+ /// Called only after supervised termination and reaping prove local quiescence.
+ pub(super) async fn stopped(&mut self) -> Result<(), DurabilityError> {
+ self.state_mut().process = AcpProcessState::Stopped;
+ self.persist().await
+ }
+
+ pub(super) async fn ready(
+ &mut self,
+ session_id: String,
+ initialization: Value,
+ enabled_operations: Vec<
+ crate::opensymphony_gateway_schema::capability::HarnessOperationCapability,
+ >,
+ model_selection: bool,
+ recovery: AcpRecovery,
+ reset_reason: Option,
+ ) -> Result<(), DurabilityError> {
+ self.require_quiescent()?;
+ if !valid_id(&session_id) {
+ return Err(DurabilityError::InvalidMetadata);
+ }
+ self.manifest.conversation_id = session_id.clone();
+ self.manifest.fresh_conversation = recovery == AcpRecovery::Fresh;
+ self.manifest.reset_reason = reset_reason;
+ self.manifest.last_attached_at = Some(chrono::Utc::now());
+ let state = self.state_mut();
+ state.session_id = Some(session_id);
+ state.initialization = initialization;
+ state.enabled_operations = enabled_operations;
+ state.model_selection = model_selection;
+ state.recovery = recovery;
+ if recovery == AcpRecovery::Fresh {
+ state.workflow_prompt_seeded = Some(false);
+ }
+ state.status = AcpSessionStatus::Ready;
+ state.stop_reason = None;
+ self.persist().await
+ }
+
+ pub(super) async fn set_model_selection(
+ &mut self,
+ model_selection: bool,
+ ) -> Result<(), DurabilityError> {
+ self.require_quiescent()?;
+ if self.state().model_selection != model_selection {
+ self.state_mut().model_selection = model_selection;
+ self.persist().await?;
+ }
+ Ok(())
+ }
+
+ pub(super) async fn submitted(
+ &mut self,
+ run_id: String,
+ attempt: u32,
+ ) -> Result<(), DurabilityError> {
+ self.require_quiescent()?;
+ if !valid_id(&run_id) || self.state().session_id.is_none() {
+ return Err(DurabilityError::InvalidMetadata);
+ }
+ let state = self.state_mut();
+ state.identity.run_id = run_id;
+ state.identity.attempt = attempt;
+ state.status = AcpSessionStatus::Submitted;
+ state.stop_reason = None;
+ self.persist().await
+ }
+
+ pub(super) async fn finished(&mut self, stop_reason: String) -> Result<(), DurabilityError> {
+ if self.state().status != AcpSessionStatus::Submitted || !valid_id(&stop_reason) {
+ return Err(DurabilityError::InvalidMetadata);
+ }
+ self.manifest.fresh_conversation = false;
+ let state = self.state_mut();
+ state.status = AcpSessionStatus::Finished;
+ if stop_reason != "cancelled_before_prompt" {
+ state.workflow_prompt_seeded = Some(true);
+ }
+ state.stop_reason = Some(stop_reason);
+ self.persist().await
+ }
+
+ pub(super) async fn uncertain(&mut self) -> Result<(), DurabilityError> {
+ self.state_mut().status = AcpSessionStatus::Uncertain;
+ self.persist().await
+ }
+
+ fn require_quiescent(&self) -> Result<(), DurabilityError> {
+ match self.state().status {
+ AcpSessionStatus::Ready | AcpSessionStatus::Finished => Ok(()),
+ AcpSessionStatus::Submitted | AcpSessionStatus::Uncertain => {
+ Err(DurabilityError::Uncertain)
+ }
+ }
+ }
+}
+
+fn valid_id(id: &str) -> bool {
+ !id.is_empty() && id.len() <= MAX_ID_BYTES && !id.chars().any(char::is_control)
+}
+
+fn verify_prior_process(process: AcpProcessState) -> Result<(), DurabilityError> {
+ let pid = match process {
+ AcpProcessState::Stopped => return Ok(()),
+ AcpProcessState::LaunchPending => return Err(DurabilityError::ProcessUncertain),
+ AcpProcessState::Running { pid } => pid,
+ };
+ #[cfg(unix)]
+ {
+ let group = i32::try_from(pid)
+ .ok()
+ .and_then(rustix::process::Pid::from_raw)
+ .ok_or(DurabilityError::InvalidMetadata)?;
+ // Only a missing process group proves local quiescence. EPERM and PID
+ // reuse remain fenced; the lock alone does not prove an orphan exited.
+ match rustix::process::test_kill_process_group(group) {
+ Err(rustix::io::Errno::SRCH) => Ok(()),
+ _ => Err(DurabilityError::Owned),
+ }
+ }
+ #[cfg(windows)]
+ {
+ // The mandatory kill-on-close JobObject ends children on owner death.
+ let _ = pid;
+ Ok(())
+ }
+ #[cfg(not(any(unix, windows)))]
+ {
+ let _ = pid;
+ Err(DurabilityError::Owned)
+ }
+}
+
+fn validate_identity(identity: &AcpSessionIdentity) -> Result<(), DurabilityError> {
+ if !valid_id(&identity.profile_id)
+ || !valid_id(&identity.profile_fingerprint)
+ || !valid_id(&identity.credential_scope)
+ || !valid_id(&identity.run_id)
+ || identity
+ .checkout_generation
+ .as_deref()
+ .is_some_and(|id| !valid_id(id))
+ || !identity.workspace_path.is_absolute()
+ {
+ return Err(DurabilityError::InvalidMetadata);
+ }
+ Ok(())
+}
+
+fn validate_binding(
+ stored: &AcpSessionIdentity,
+ requested: &AcpSessionIdentity,
+) -> Result<(), DurabilityError> {
+ validate_static_binding(stored, requested)?;
+ for (matches, name) in [
+ (
+ stored.profile_fingerprint == requested.profile_fingerprint,
+ "profile fingerprint",
+ ),
+ (
+ stored.credential_scope == requested.credential_scope,
+ "credential scope",
+ ),
+ ] {
+ if !matches {
+ return Err(DurabilityError::Binding(name));
+ }
+ }
+ Ok(())
+}
+
+fn validate_static_binding(
+ stored: &AcpSessionIdentity,
+ requested: &AcpSessionIdentity,
+) -> Result<(), DurabilityError> {
+ for (matches, name) in [
+ (stored.profile_id == requested.profile_id, "profile"),
+ (
+ stored.workspace_path == requested.workspace_path,
+ "workspace",
+ ),
+ (
+ stored.repository_binding == requested.repository_binding,
+ "repository",
+ ),
+ (
+ stored.checkout_generation == requested.checkout_generation,
+ "checkout generation",
+ ),
+ ] {
+ if !matches {
+ return Err(DurabilityError::Binding(name));
+ }
+ }
+ Ok(())
+}
+
+async fn sync_directory(path: &Path) -> Result<(), DurabilityError> {
+ #[cfg(unix)]
+ tokio::fs::File::open(path).await?.sync_all().await?;
+ // Windows workspace replacement uses MoveFileExW(MOVEFILE_WRITE_THROUGH).
+ #[cfg(not(unix))]
+ let _ = path;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::opensymphony_workspace::{
+ CleanupConfig, HookConfig, IssueDescriptor, WorkspaceManagerConfig,
+ };
+ use serde_json::json;
+
+ fn manager(root: &Path) -> WorkspaceManager {
+ WorkspaceManager::new(WorkspaceManagerConfig {
+ root: root.to_path_buf(),
+ hooks: HookConfig::default(),
+ cleanup: CleanupConfig::default(),
+ })
+ .expect("manager")
+ }
+
+ async fn workspace(root: &Path) -> WorkspaceHandle {
+ manager(root)
+ .ensure(&IssueDescriptor {
+ issue_id: "issue-609".into(),
+ identifier: "COE-609".into(),
+ title: "durability".into(),
+ current_state: "In Progress".into(),
+ last_seen_tracker_refresh_at: None,
+ repository_binding: None,
+ })
+ .await
+ .expect("workspace")
+ .handle
+ }
+
+ fn identity(workspace: &WorkspaceHandle) -> AcpSessionIdentity {
+ AcpSessionIdentity {
+ profile_id: "agent".into(),
+ profile_fingerprint: "a".repeat(64),
+ credential_scope: "grant-revision-1".into(),
+ workspace_path: workspace.workspace_path().to_path_buf(),
+ repository_binding: None,
+ checkout_generation: None,
+ generation: 0,
+ run_id: "run-1".into(),
+ attempt: 1,
+ }
+ }
+
+ #[tokio::test]
+ async fn owner_claim_is_exclusive_across_processes_and_reserves_generation() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let input = identity(&workspace);
+ let mut first = Durability::open(manager(root.path()), workspace.clone(), input.clone())
+ .await
+ .expect("owner");
+ assert_eq!(first.state().identity.generation, 1);
+ assert!(matches!(
+ Durability::open(manager(root.path()), workspace.clone(), input.clone()).await,
+ Err(DurabilityError::Owned)
+ ));
+ let output = tokio::time::timeout(
+ std::time::Duration::from_secs(10),
+ tokio::process::Command::new(std::env::current_exe().expect("test binary"))
+ .args([
+ "--exact",
+ "opensymphony_acp::durable::tests::owner_lock_child",
+ "--nocapture",
+ ])
+ .env(
+ "ACP_TEST_OWNER_LOCK",
+ workspace.metadata_dir().join("acp-owner.lock"),
+ )
+ .output(),
+ )
+ .await
+ .expect("child deadline")
+ .expect("child");
+ assert!(
+ output.status.success(),
+ "{}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ assert!(String::from_utf8_lossy(&output.stdout).contains("1 passed"));
+ first.stopped().await.expect("first owner quiescent");
+ drop(first);
+ let mut second = Durability::open(manager(root.path()), workspace.clone(), input.clone())
+ .await
+ .expect("next owner");
+ assert_eq!(second.state().identity.generation, 2);
+ second.stopped().await.expect("second owner quiescent");
+ drop(second);
+ let mut stale = input;
+ stale.generation = 1;
+ let stale_result = Durability::open(manager(root.path()), workspace, stale)
+ .await
+ .map(|_| ());
+ assert!(
+ matches!(
+ stale_result,
+ Err(DurabilityError::Binding("connection generation"))
+ ),
+ "{stale_result:?}"
+ );
+ }
+
+ #[tokio::test]
+ async fn new_claim_clears_prior_terminal_outcome_before_launch() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let first_identity = identity(&workspace);
+ let mut first = Durability::open(
+ manager(root.path()),
+ workspace.clone(),
+ first_identity.clone(),
+ )
+ .await
+ .expect("first owner");
+ first
+ .ready(
+ "session-1".into(),
+ Value::Null,
+ Vec::new(),
+ false,
+ AcpRecovery::Fresh,
+ None,
+ )
+ .await
+ .expect("ready first turn");
+ first
+ .submitted(first_identity.run_id, first_identity.attempt)
+ .await
+ .expect("submit first turn");
+ first
+ .finished("end_turn".into())
+ .await
+ .expect("first terminal outcome");
+ first.stopped().await.expect("quiescent");
+ drop(first);
+
+ let mut next_identity = identity(&workspace);
+ next_identity.run_id = "run-2".into();
+ next_identity.attempt = 2;
+ let next = Durability::open(manager(root.path()), workspace.clone(), next_identity)
+ .await
+ .expect("next owner");
+ assert_eq!(next.state().process, AcpProcessState::LaunchPending);
+ assert_eq!(next.state().status, AcpSessionStatus::Ready);
+ assert_eq!(next.state().stop_reason, None);
+ assert!(next.state().workflow_prompt_seeded());
+ let persisted = manager(root.path())
+ .load_conversation_manifest(&workspace)
+ .await
+ .expect("manifest")
+ .expect("manifest")
+ .acp
+ .expect("ACP state");
+ assert_eq!(persisted.identity.run_id, "run-2");
+ assert_eq!(persisted.identity.attempt, 2);
+ assert_eq!(persisted.status, AcpSessionStatus::Ready);
+ assert_eq!(persisted.stop_reason, None);
+ assert_eq!(persisted.workflow_prompt_seeded, Some(true));
+ }
+
+ #[tokio::test]
+ async fn legacy_finished_prompt_is_migrated_before_new_run_claim() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let input = identity(&workspace);
+ let mut first = Durability::open(manager(root.path()), workspace.clone(), input.clone())
+ .await
+ .expect("first owner");
+ first
+ .ready(
+ "session-1".into(),
+ Value::Null,
+ Vec::new(),
+ false,
+ AcpRecovery::Fresh,
+ None,
+ )
+ .await
+ .expect("ready");
+ first
+ .submitted(input.run_id, input.attempt)
+ .await
+ .expect("submitted");
+ first.finished("end_turn".into()).await.expect("finished");
+ first.stopped().await.expect("stopped");
+ drop(first);
+ let mut legacy = manager(root.path())
+ .load_conversation_manifest(&workspace)
+ .await
+ .expect("manifest")
+ .expect("conversation");
+ legacy.acp.as_mut().expect("ACP").workflow_prompt_seeded = None;
+ manager(root.path())
+ .write_json_artifact_atomically(
+ &workspace,
+ &workspace.conversation_manifest_path(),
+ &legacy,
+ )
+ .await
+ .expect("legacy manifest");
+
+ let mut next = identity(&workspace);
+ next.run_id = "run-2".into();
+ next.attempt = 2;
+ let claimed = Durability::open(manager(root.path()), workspace, next)
+ .await
+ .expect("second owner");
+ assert_eq!(claimed.state().status, AcpSessionStatus::Ready);
+ assert_eq!(claimed.state().workflow_prompt_seeded, Some(true));
+ }
+
+ #[tokio::test]
+ async fn pre_prompt_cancellation_does_not_seed_workflow_context() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let input = identity(&workspace);
+ let mut owner = Durability::open(manager(root.path()), workspace, input.clone())
+ .await
+ .expect("owner");
+ owner
+ .ready(
+ "session-1".into(),
+ Value::Null,
+ Vec::new(),
+ false,
+ AcpRecovery::Fresh,
+ None,
+ )
+ .await
+ .expect("ready");
+ owner
+ .submitted(input.run_id, input.attempt)
+ .await
+ .expect("submitted");
+ owner
+ .finished("cancelled_before_prompt".into())
+ .await
+ .expect("cancelled");
+ assert!(!owner.state().workflow_prompt_seeded());
+ }
+
+ #[test]
+ fn owner_lock_child() {
+ let Some(path) = std::env::var_os("ACP_TEST_OWNER_LOCK") else {
+ return;
+ };
+ let file = File::options()
+ .read(true)
+ .write(true)
+ .open(path)
+ .expect("lock file");
+ assert!(matches!(
+ file.try_lock(),
+ Err(std::fs::TryLockError::WouldBlock)
+ ));
+ }
+
+ #[tokio::test]
+ async fn crash_during_launch_retains_an_explicit_ownership_fence() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let input = identity(&workspace);
+ let owner = Durability::open(manager(root.path()), workspace.clone(), input.clone())
+ .await
+ .expect("owner");
+ assert_eq!(owner.state().process, AcpProcessState::LaunchPending);
+ drop(owner);
+ let result = Durability::open(manager(root.path()), workspace, input)
+ .await
+ .map(|_| ());
+ assert!(
+ matches!(result, Err(DurabilityError::ProcessUncertain)),
+ "{result:?}"
+ );
+ }
+
+ #[cfg(unix)]
+ #[tokio::test]
+ async fn owner_loss_refuses_a_surviving_process_group_and_allows_a_dead_one() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let input = identity(&workspace);
+ let mut owner = Durability::open(manager(root.path()), workspace.clone(), input.clone())
+ .await
+ .expect("owner");
+ let mut command = tokio::process::Command::new("sleep");
+ command.arg("30").kill_on_drop(true);
+ crate::opensymphony_workspace::configure_process_group(&mut command);
+ let mut child = command.spawn().expect("process group");
+ owner
+ .launched(child.id().expect("child pid"))
+ .await
+ .expect("launch checkpoint");
+ drop(owner);
+ assert!(matches!(
+ Durability::open(manager(root.path()), workspace.clone(), input.clone()).await,
+ Err(DurabilityError::Owned)
+ ));
+ child.kill().await.expect("stop and reap orphan");
+ let mut recovered = Durability::open(manager(root.path()), workspace, input)
+ .await
+ .expect("dead group can recover");
+ assert_eq!(recovered.state().identity.generation, 2);
+ recovered.stopped().await.expect("release reservation");
+ }
+
+ #[tokio::test]
+ async fn submission_is_durable_and_blocks_recovery_until_terminal_evidence() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let input = identity(&workspace);
+ let mut owner = Durability::open(manager(root.path()), workspace.clone(), input.clone())
+ .await
+ .expect("owner");
+ owner
+ .ready(
+ "opaque/session:1".into(),
+ json!({"protocolVersion": 1}),
+ Vec::new(),
+ false,
+ AcpRecovery::Fresh,
+ None,
+ )
+ .await
+ .expect("ready");
+ owner
+ .submitted("run-2".into(), 2)
+ .await
+ .expect("submission barrier");
+ let stored = manager(root.path())
+ .load_conversation_manifest(&workspace)
+ .await
+ .expect("load")
+ .expect("manifest")
+ .acp
+ .expect("ACP");
+ assert_eq!(stored.status, AcpSessionStatus::Submitted);
+ assert_eq!(stored.identity.run_id, "run-2");
+ assert_eq!(stored.identity.attempt, 2);
+ assert!(matches!(
+ owner
+ .ready(
+ "replacement".into(),
+ Value::Null,
+ Vec::new(),
+ false,
+ AcpRecovery::Fresh,
+ Some("reset".into())
+ )
+ .await,
+ Err(DurabilityError::Uncertain)
+ ));
+ drop(owner);
+ assert!(matches!(
+ Durability::open(manager(root.path()), workspace.clone(), input.clone()).await,
+ Err(DurabilityError::Uncertain)
+ ));
+
+ // An explicit host resolution updates the existing record, preserving identity.
+ let mut manifest = manager(root.path())
+ .load_conversation_manifest(&workspace)
+ .await
+ .expect("load")
+ .expect("manifest");
+ manifest.acp.as_mut().expect("ACP").status = AcpSessionStatus::Finished;
+ manifest.acp.as_mut().expect("ACP").process = AcpProcessState::Stopped;
+ manager(root.path())
+ .write_json_artifact_atomically(
+ &workspace,
+ &workspace.conversation_manifest_path(),
+ &manifest,
+ )
+ .await
+ .expect("resolve");
+ let mut restored = Durability::open(manager(root.path()), workspace.clone(), input.clone())
+ .await
+ .expect("resolved owner");
+ assert_eq!(
+ restored.state().session_id.as_deref(),
+ Some("opaque/session:1")
+ );
+ restored
+ .ready(
+ "opaque/session:1".into(),
+ json!({"protocolVersion": 1}),
+ Vec::new(),
+ false,
+ AcpRecovery::RestoredLoad,
+ None,
+ )
+ .await
+ .expect("load ready");
+ restored
+ .submitted("run-3".into(), 3)
+ .await
+ .expect("next submission");
+ restored
+ .finished("end_turn".into())
+ .await
+ .expect("terminal evidence");
+ restored.stopped().await.expect("supervised process reaped");
+ drop(restored);
+ let mut finished_identity = input.clone();
+ finished_identity.run_id = "run-3".into();
+ finished_identity.attempt = 3;
+ let mut reopened =
+ Durability::open(manager(root.path()), workspace.clone(), finished_identity)
+ .await
+ .expect("finished recovery");
+ assert_eq!(reopened.state().status, AcpSessionStatus::Finished);
+ assert_eq!(reopened.state().stop_reason.as_deref(), Some("end_turn"));
+ reopened.uncertain().await.expect("explicit risk fence");
+ drop(reopened);
+ assert!(matches!(
+ Durability::open(manager(root.path()), workspace, input).await,
+ Err(DurabilityError::Uncertain)
+ ));
+ }
+
+ #[tokio::test]
+ async fn reuse_checks_profile_scope_workspace_repository_and_checkout_binding() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let input = identity(&workspace);
+ let mut owner = Durability::open(manager(root.path()), workspace.clone(), input.clone())
+ .await
+ .expect("owner");
+ owner.stopped().await.expect("owner quiescent");
+ drop(owner);
+ for field in [
+ "profile",
+ "profile fingerprint",
+ "credential scope",
+ "workspace",
+ "repository",
+ "checkout generation",
+ ] {
+ let mut changed = input.clone();
+ match field {
+ "profile" => changed.profile_id.push('2'),
+ "profile fingerprint" => changed.profile_fingerprint.push('2'),
+ "credential scope" => changed.credential_scope.push('2'),
+ "workspace" => changed.workspace_path.push("other"),
+ "repository" => changed.repository_binding = Some(serde_json::from_value(json!({
+ "alias": "other",
+ "repository": {"id": "github:repository:other", "safe_remote_fingerprint": "sha256:other"},
+ "config_generation": "config-1",
+ "inventory_generation": "inventory-1"
+ })).expect("repository binding")),
+ "checkout generation" => changed.checkout_generation = Some("checkout-2".into()),
+ _ => unreachable!(),
+ }
+ assert!(
+ matches!(Durability::open(manager(root.path()), workspace.clone(), changed).await, Err(DurabilityError::Binding(name)) if name == field),
+ "{field}"
+ );
+ }
+ let mut manifest = manager(root.path())
+ .load_conversation_manifest(&workspace)
+ .await
+ .expect("load")
+ .expect("manifest");
+ manifest.issue_id = "another-issue".into();
+ manager(root.path())
+ .write_conversation_manifest(&workspace, &manifest)
+ .await
+ .expect("foreign issue");
+ assert!(matches!(
+ Durability::open(manager(root.path()), workspace, input).await,
+ Err(DurabilityError::Binding("issue"))
+ ));
+ }
+
+ #[tokio::test]
+ async fn native_manifests_remain_readable_and_are_not_overwritten() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let native = ConversationManifest::new(
+ &workspace,
+ "native-session",
+ "http://localhost",
+ workspace.metadata_dir(),
+ "native-v1",
+ );
+ let raw = serde_json::to_value(&native).expect("native serialization");
+ assert!(raw.get("acp").is_none());
+ let decoded: ConversationManifest = serde_json::from_value(raw).expect("legacy manifest");
+ assert!(decoded.acp.is_none());
+ manager(root.path())
+ .write_conversation_manifest(&workspace, &native)
+ .await
+ .expect("native write");
+ assert!(matches!(
+ Durability::open(
+ manager(root.path()),
+ workspace.clone(),
+ identity(&workspace)
+ )
+ .await,
+ Err(DurabilityError::NativeManifest)
+ ));
+ assert_eq!(
+ manager(root.path())
+ .load_conversation_manifest(&workspace)
+ .await
+ .expect("read")
+ .expect("native"),
+ native
+ );
+ }
+
+ #[tokio::test]
+ async fn invalid_or_oversized_metadata_never_replaces_the_durable_record() {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let mut owner = Durability::open(
+ manager(root.path()),
+ workspace.clone(),
+ identity(&workspace),
+ )
+ .await
+ .expect("owner");
+ let before = std::fs::read(workspace.conversation_manifest_path()).expect("reservation");
+ assert!(matches!(
+ owner
+ .ready(
+ "bad\nsession".into(),
+ Value::Null,
+ Vec::new(),
+ false,
+ AcpRecovery::Fresh,
+ None
+ )
+ .await,
+ Err(DurabilityError::InvalidMetadata)
+ ));
+ assert!(matches!(
+ owner
+ .ready(
+ "session".into(),
+ json!("x".repeat(MAX_MANIFEST_BYTES)),
+ Vec::new(),
+ false,
+ AcpRecovery::Fresh,
+ None
+ )
+ .await,
+ Err(DurabilityError::InvalidMetadata)
+ ));
+ assert_eq!(
+ std::fs::read(workspace.conversation_manifest_path()).expect("reservation remains"),
+ before
+ );
+ }
+
+ #[cfg(unix)]
+ #[tokio::test]
+ async fn rejects_symlinked_lock_and_manifest_without_touching_the_target() {
+ for filename in ["acp-owner.lock", "conversation.json"] {
+ let root = tempfile::tempdir().expect("temp");
+ let workspace = workspace(root.path()).await;
+ let outside = root.path().join("outside");
+ std::fs::write(&outside, "unchanged").expect("outside");
+ std::os::unix::fs::symlink(&outside, workspace.metadata_dir().join(filename))
+ .expect("symlink");
+ assert!(matches!(
+ Durability::open(
+ manager(root.path()),
+ workspace.clone(),
+ identity(&workspace)
+ )
+ .await,
+ Err(DurabilityError::Workspace(
+ WorkspaceError::ManagedPathSymlink { .. }
+ ))
+ ));
+ assert_eq!(
+ std::fs::read_to_string(outside).expect("outside remains"),
+ "unchanged"
+ );
+ }
+ }
+}
diff --git a/crates/opensymphony-acp/src/extensions.rs b/crates/opensymphony-acp/src/extensions.rs
new file mode 100644
index 000000000..dd7442c98
--- /dev/null
+++ b/crates/opensymphony-acp/src/extensions.rs
@@ -0,0 +1,207 @@
+//! Explicit ACP extension registrations. A profile opts into reviewed wire contracts;
+//! an agent's advertised support never grants an operator permission by itself.
+use serde_json::{Value, json};
+
+use super::AcpProfile;
+pub use crate::opensymphony_gateway_schema::capability::HarnessOperationCapability as OperationCapability;
+
+pub const CURSOR_VERSION: &str = "cursor@2026.09.08-6caf4ff";
+pub const FIXTURE_ECHO_VERSION: &str = "fixture_echo@1";
+pub const FIXTURE_ECHO_OPERATION: &str = "fixture.echo";
+pub const FIXTURE_ECHO_METHOD: &str = "_opensymphony.test/echo";
+
+fn metadata_schema() -> Value {
+ json!({
+ "type": "object",
+ "maxProperties": 8,
+ "propertyNames": {
+ "allOf": [
+ {"maxLength": 128},
+ {"anyOf": [{"pattern": "/"}, {"enum": ["traceparent", "tracestate", "baggage"]}]}
+ ]
+ }
+ })
+}
+
+pub fn cursor_enabled(profile: &AcpProfile) -> bool {
+ profile.extensions.iter().any(|id| id == CURSOR_VERSION)
+}
+
+pub fn cursor_todos(params: &Value) -> bool {
+ let Some(object) = params.as_object() else {
+ return false;
+ };
+ if object.len() > 16 || !serde_json::to_vec(params).is_ok_and(|bytes| bytes.len() <= 64 * 1024)
+ {
+ return false;
+ }
+ let bounded = |key: &str, max: usize| {
+ object
+ .get(key)
+ .and_then(Value::as_str)
+ .is_some_and(|value| !value.is_empty() && value.len() <= max)
+ };
+ bounded("toolCallId", 128)
+ && object
+ .get("todos")
+ .and_then(Value::as_array)
+ .is_some_and(|todos| {
+ todos.len() <= 128
+ && todos.iter().all(|todo| {
+ todo.get("id")
+ .and_then(Value::as_str)
+ .is_some_and(|id| !id.is_empty() && id.len() <= 128)
+ && todo
+ .get("content")
+ .and_then(Value::as_str)
+ .is_some_and(|content| content.len() <= 2048)
+ && todo
+ .get("status")
+ .and_then(Value::as_str)
+ .is_some_and(|status| {
+ matches!(
+ status,
+ "pending" | "in_progress" | "completed" | "cancelled"
+ )
+ })
+ })
+ })
+ && object.get("merge").is_some_and(Value::is_boolean)
+}
+
+pub fn fixture_echo_enabled(profile: &AcpProfile, initialization: &Value) -> bool {
+ profile
+ .extensions
+ .iter()
+ .any(|id| id == FIXTURE_ECHO_VERSION)
+ && initialization.pointer("/agentCapabilities/_meta/opensymphony.dev~1fixtureEcho")
+ == Some(&json!(1))
+}
+
+pub fn outbound_operation(
+ profile: &AcpProfile,
+ initialization: &Value,
+ operation_id: &str,
+) -> Option {
+ if operation_id != FIXTURE_ECHO_OPERATION || !fixture_echo_enabled(profile, initialization) {
+ return None;
+ }
+ Some(OperationCapability {
+ operation_id: FIXTURE_ECHO_OPERATION.into(),
+ namespace: "opensymphony.test".into(),
+ version: "1".into(),
+ capability_predicate: "agentCapabilities._meta['opensymphony.dev/fixtureEcho'] == 1".into(),
+ parameters_schema: json!({"type":"object","required":["value"],"properties":{"value":{"type":"string","maxLength":4096},"_meta":metadata_schema()},"additionalProperties":false}),
+ result_schema: json!({"type":"object","required":["value"],"properties":{"value":{"type":"string","maxLength":4096},"_meta":metadata_schema()},"additionalProperties":false}),
+ deadline_ms: 5000,
+ effect: "read_only_idempotent".into(),
+ })
+}
+
+pub fn configured_operations(profile: &AcpProfile) -> Vec {
+ if !profile
+ .extensions
+ .iter()
+ .any(|id| id == FIXTURE_ECHO_VERSION)
+ {
+ return Vec::new();
+ }
+ // This describes the predicate for IDE negotiation; active use also checks
+ // the peer's negotiated capability and the current session binding.
+ outbound_operation(
+ profile,
+ &json!({"agentCapabilities":{"_meta":{"opensymphony.dev/fixtureEcho":1}}}),
+ FIXTURE_ECHO_OPERATION,
+ )
+ .into_iter()
+ .collect()
+}
+
+pub fn validate_echo_arguments(arguments: &Value) -> bool {
+ let Some(object) = arguments.as_object() else {
+ return false;
+ };
+ if object.len() > 2
+ || !object.contains_key("value")
+ || object.keys().any(|key| key != "value" && key != "_meta")
+ {
+ return false;
+ }
+ if !object
+ .get("value")
+ .and_then(Value::as_str)
+ .is_some_and(|value| value.len() <= 4096)
+ {
+ return false;
+ }
+ object.get("_meta").is_none_or(|meta| {
+ meta.as_object().is_some_and(|fields| {
+ fields.len() <= 8
+ && fields.iter().all(|(key, value)| {
+ (key.contains('/')
+ || matches!(key.as_str(), "traceparent" | "tracestate" | "baggage"))
+ && key.len() <= 128
+ && serde_json::to_vec(value).is_ok_and(|bytes| bytes.len() <= 4096)
+ })
+ })
+ })
+}
+
+pub fn validate_echo_result(result: &Value) -> bool {
+ result.as_object().is_some_and(|object| {
+ object.len() <= 2
+ && object
+ .get("value")
+ .and_then(Value::as_str)
+ .is_some_and(|value| value.len() <= 4096)
+ && object.keys().all(|key| key == "value" || key == "_meta")
+ && object.get("_meta").is_none_or(|meta| {
+ meta.as_object().is_some_and(|fields| {
+ fields.len() <= 8
+ && fields.iter().all(|(key, value)| {
+ (key.contains('/')
+ || matches!(key.as_str(), "traceparent" | "tracestate" | "baggage"))
+ && key.len() <= 128
+ && serde_json::to_vec(value).is_ok_and(|bytes| bytes.len() <= 4096)
+ })
+ })
+ })
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn echo_requires_registered_profile_and_peer_capability() {
+ let mut profile: AcpProfile =
+ serde_json::from_value(json!({"command":"peer"})).expect("fixture profile");
+ let advertised = json!({"agentCapabilities":{"_meta":{"opensymphony.dev/fixtureEcho":1}}});
+ assert!(outbound_operation(&profile, &advertised, FIXTURE_ECHO_OPERATION).is_none());
+ profile.extensions.push(FIXTURE_ECHO_VERSION.into());
+ assert!(outbound_operation(&profile, &json!({}), FIXTURE_ECHO_OPERATION).is_none());
+ let operation = outbound_operation(&profile, &advertised, FIXTURE_ECHO_OPERATION)
+ .expect("negotiated operation");
+ assert_eq!(
+ operation.result_schema["properties"]["_meta"]["type"],
+ "object"
+ );
+ assert_eq!(
+ operation.result_schema["properties"]["_meta"]["maxProperties"],
+ 8
+ );
+ assert!(validate_echo_arguments(
+ &json!({"value":"ok","_meta":{"traceparent":"trace"}})
+ ));
+ assert!(!validate_echo_arguments(
+ &json!({"value":"ok","method":"other"})
+ ));
+ assert!(validate_echo_result(&json!({"value":"ok"})));
+ assert!(!validate_echo_result(&json!({"value":42})));
+ assert!(!validate_echo_result(&json!({"value":"ok","execute":true})));
+ assert!(!validate_echo_arguments(
+ &json!({"value":"ok","_meta":{"command":"unsafe"}})
+ ));
+ }
+}
diff --git a/crates/opensymphony-acp/src/host.rs b/crates/opensymphony-acp/src/host.rs
new file mode 100644
index 000000000..a5498a321
--- /dev/null
+++ b/crates/opensymphony-acp/src/host.rs
@@ -0,0 +1,1269 @@
+//! Bounded runtime ownership. Scheduler decisions remain outside this actor.
+use super::{
+ AcpProfile, ClientError, ClientLimits, LaunchContext, SharedCapture, SourceFrame, TurnReport,
+ durable::{Durability, DurabilityError, profile_fingerprint},
+};
+use crate::opensymphony_workspace::{
+ AcpRecovery, AcpSessionIdentity, AcpSessionState, AcpSessionStatus, WorkspaceHandle,
+ WorkspaceManager,
+};
+use agent_client_protocol::{
+ Agent, ConnectionTo, UntypedMessage,
+ schema::v1::{
+ CancelNotification, ContentBlock, InitializeResponse, PromptRequest, SessionId, TextContent,
+ },
+};
+use futures_util::{StreamExt, stream::FuturesUnordered};
+use serde::{Deserialize, Serialize};
+use std::{
+ collections::{BTreeMap, VecDeque},
+ future::Future,
+ pin::Pin,
+ sync::{Arc, Mutex},
+ time::Duration,
+};
+use tokio::sync::{broadcast, mpsc, oneshot};
+use tokio_util::sync::CancellationToken;
+
+#[derive(Debug, Clone)]
+pub struct RetentionPolicy {
+ pub max_sessions: usize,
+ pub idle_timeout: Duration,
+ pub attachment_ttl: Duration,
+ pub max_attachments: usize,
+}
+impl Default for RetentionPolicy {
+ fn default() -> Self {
+ Self {
+ max_sessions: 8,
+ idle_timeout: Duration::from_secs(900),
+ attachment_ttl: Duration::from_secs(60),
+ max_attachments: 16,
+ }
+ }
+}
+
+/// Retire a durable, known-quiescent session after its original host is gone.
+/// The owner lock and prior process check must both succeed; this never launches
+/// a peer or resolves an ambiguous prompt by assuming its process exited.
+pub async fn retire_persisted_session(
+ manager: WorkspaceManager,
+ workspace: WorkspaceHandle,
+) -> Result<(), HostError> {
+ let state = manager
+ .load_conversation_manifest(&workspace)
+ .await
+ .map_err(|_| HostError::Persistence)?
+ .and_then(|manifest| manifest.acp)
+ .ok_or(HostError::NativeManifest)?;
+ Durability::retire_persisted(manager, workspace, state.identity).await?;
+ Ok(())
+}
+
+/// Host callers supply the scheduler's verified workspace and current credential/grant revision.
+pub struct SessionLaunch {
+ pub manager: WorkspaceManager,
+ pub workspace: WorkspaceHandle,
+ pub identity: AcpSessionIdentity,
+ pub profile: AcpProfile,
+ pub context: LaunchContext,
+ pub limits: ClientLimits,
+ pub require_persistence: bool,
+ /// A parent continuation may refresh its process credentials only while
+ /// retaining this already-authoritative ACP session binding.
+ pub expected_session_id: Option,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error, Serialize, Deserialize)]
+pub enum HostError {
+ #[error("ACP owner is unavailable; inspect persisted state before recovery")]
+ Unavailable,
+ #[error("ACP session identity or connection generation does not match")]
+ IdentityMismatch,
+ #[error("ACP session has active work or attachment leases")]
+ Busy,
+ #[error("ACP host or subscriber resource limit reached")]
+ ResourceLimit,
+ #[error("ACP durable session reservation or checkpoint failed")]
+ Persistence,
+ #[error("ACP session has a live owner or prior process; attach through that host")]
+ AlreadyOwned,
+ #[error(
+ "ACP prior prompt delivery is uncertain; reconcile execution risk before another attempt"
+ )]
+ UncertainSubmission,
+ #[error("ACP host was lost during process launch; reconcile process ownership before retrying")]
+ UncertainLaunch,
+ #[error("ACP cannot replace a native conversation manifest")]
+ NativeManifest,
+ #[error("ACP session cannot restore context with negotiated capabilities")]
+ PersistenceUnsupported,
+ #[error("ACP session launch or setup failed: {0}")]
+ Client(String),
+}
+
+impl From for HostError {
+ fn from(error: DurabilityError) -> Self {
+ match error {
+ DurabilityError::Owned => Self::AlreadyOwned,
+ DurabilityError::Uncertain => Self::UncertainSubmission,
+ DurabilityError::ProcessUncertain => Self::UncertainLaunch,
+ DurabilityError::NativeManifest => Self::NativeManifest,
+ DurabilityError::Binding(_) => Self::IdentityMismatch,
+ _ => Self::Persistence,
+ }
+ }
+}
+
+/// Read recorded evidence without claiming that agent context or a live owner was restored.
+pub async fn inspect_recorded_session(
+ manager: &WorkspaceManager,
+ workspace: &WorkspaceHandle,
+) -> Result {
+ let manifest = manager
+ .load_conversation_manifest(workspace)
+ .await
+ .map_err(|_| HostError::Persistence)?
+ .ok_or(HostError::Unavailable)?;
+ if manifest.issue_id != workspace.issue_id() || manifest.identifier != workspace.identifier() {
+ return Err(HostError::IdentityMismatch);
+ }
+ let mut state = manifest.acp.ok_or(HostError::NativeManifest)?;
+ if state.identity.workspace_path != workspace.workspace_path() {
+ return Err(HostError::IdentityMismatch);
+ }
+ state.recovery = AcpRecovery::TranscriptOnly;
+ Ok(SessionSnapshot {
+ state,
+ live: false,
+ attachment_count: 0,
+ retirement_eligible: false,
+ })
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SessionSnapshot {
+ pub state: AcpSessionState,
+ pub live: bool,
+ pub attachment_count: usize,
+ pub retirement_eligible: bool,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case")]
+pub enum SessionEvent {
+ Source {
+ generation: u64,
+ run_id: String,
+ replay: bool,
+ frame: SourceFrame,
+ },
+ State {
+ snapshot: Box,
+ },
+ Gap {
+ generation: u64,
+ sequence: u64,
+ },
+ Ended {
+ generation: u64,
+ cleanup_ready: bool,
+ detail: Option,
+ },
+}
+
+#[derive(Clone)]
+pub(super) struct EventPublisher {
+ tx: broadcast::Sender,
+ binding: Arc>,
+ replay_request: Arc>>,
+ history: Arc>,
+}
+impl EventPublisher {
+ pub(super) fn end_replay(&self) {
+ self.binding.lock().unwrap_or_else(|e| e.into_inner()).2 = false;
+ }
+ pub(super) fn publish(&self, frame: SourceFrame) {
+ if frame.direction == "outgoing" && frame.payload["method"] == "session/load" {
+ *self
+ .replay_request
+ .lock()
+ .unwrap_or_else(|e| e.into_inner()) = frame.payload.get("id").cloned();
+ }
+ let ends_replay = frame.direction == "incoming"
+ && frame.payload.get("method").is_none()
+ && frame.payload.get("id").is_some_and(|id| {
+ self.replay_request
+ .lock()
+ .unwrap_or_else(|e| e.into_inner())
+ .as_ref()
+ == Some(id)
+ });
+ let (generation, run_id, replay) = self
+ .binding
+ .lock()
+ .unwrap_or_else(|e| e.into_inner())
+ .clone();
+ let event = SessionEvent::Source {
+ generation,
+ run_id,
+ replay,
+ frame,
+ };
+ let admitted = self
+ .history
+ .lock()
+ .unwrap_or_else(|e| e.into_inner())
+ .retain(event.clone());
+ let event = if admitted {
+ event
+ } else {
+ let SessionEvent::Source { frame, .. } = event else {
+ unreachable!()
+ };
+ SessionEvent::Gap {
+ generation,
+ sequence: frame.sequence,
+ }
+ };
+ let _ = self.tx.send(event);
+ if ends_replay {
+ self.end_replay();
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct SourceHistory {
+ pub events: Vec,
+ pub truncated: bool,
+ /// Latest published source cursor, including a frame too large to retain.
+ #[serde(default)]
+ pub latest_cursor: Option<(u64, u64)>,
+}
+impl SourceHistory {
+ /// A sticky historical eviction is harmless when every frame after this
+ /// run's processed cursor still appears in the retained contiguous tail.
+ pub fn covers_since(&self, cursor: (u64, u64)) -> bool {
+ let Some(latest) = self.latest_cursor else {
+ return true;
+ };
+ if latest.0 != cursor.0 {
+ return false;
+ }
+ if latest.1 <= cursor.1 {
+ return true;
+ }
+ let Some(mut expected) = cursor.1.checked_add(1) else {
+ return false;
+ };
+ for event in &self.events {
+ if let SessionEvent::Source {
+ generation, frame, ..
+ } = event
+ && *generation == cursor.0
+ && frame.sequence >= expected
+ {
+ if frame.sequence != expected {
+ return false;
+ }
+ if expected == latest.1 {
+ return true;
+ }
+ let Some(next) = expected.checked_add(1) else {
+ return false;
+ };
+ expected = next;
+ }
+ }
+ false
+ }
+}
+struct EventHistory {
+ events: VecDeque<(SessionEvent, usize)>,
+ bytes: usize,
+ max_bytes: usize,
+ max_frames: usize,
+ truncated: bool,
+ latest_cursor: Option<(u64, u64)>,
+}
+impl EventHistory {
+ fn retain(&mut self, event: SessionEvent) -> bool {
+ if let SessionEvent::Source {
+ generation, frame, ..
+ } = &event
+ {
+ self.latest_cursor = Some((*generation, frame.sequence));
+ }
+ let bytes = serde_json::to_vec(&event).map_or(usize::MAX, |v| v.len());
+ if bytes > self.max_bytes {
+ self.truncated = true;
+ return false;
+ }
+ while self.events.len() >= self.max_frames
+ || self.bytes.saturating_add(bytes) > self.max_bytes
+ {
+ let Some((_, removed)) = self.events.pop_front() else {
+ break;
+ };
+ self.bytes -= removed;
+ self.truncated = true;
+ }
+ self.bytes += bytes;
+ self.events.push_back((event, bytes));
+ true
+ }
+}
+
+#[derive(Clone)]
+pub struct SessionHandle {
+ pub owner_id: String,
+ pub generation: u64,
+ commands: mpsc::Sender,
+ events: broadcast::Sender,
+ history: Arc>,
+ persistence_supported: bool,
+}
+impl std::fmt::Debug for SessionHandle {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("SessionHandle")
+ .field("generation", &self.generation)
+ .finish_non_exhaustive()
+ }
+}
+impl SessionHandle {
+ pub fn subscribe(&self) -> broadcast::Receiver {
+ self.events.subscribe()
+ }
+ /// Subscribe first, then read history and deduplicate source sequence numbers.
+ pub fn source_history(&self) -> SourceHistory {
+ let history = self.history.lock().unwrap_or_else(|e| e.into_inner());
+ SourceHistory {
+ events: history
+ .events
+ .iter()
+ .map(|(event, _)| event.clone())
+ .collect(),
+ truncated: history.truncated,
+ latest_cursor: history.latest_cursor,
+ }
+ }
+ pub async fn inspect(&self) -> Result {
+ match self.control(SessionControl::Inspect).await? {
+ ControlResult::Snapshot(s) => Ok(*s),
+ _ => Err(HostError::Unavailable),
+ }
+ }
+ /// Observation pins the live process but grants no prompt/write authority.
+ pub async fn control(&self, action: SessionControl) -> Result {
+ let (reply, receive) = oneshot::channel();
+ self.commands
+ .try_send(Command::Control {
+ generation: self.generation,
+ action,
+ reply,
+ })
+ .map_err(channel_error)?;
+ receive.await.map_err(|_| HostError::Unavailable)?
+ }
+ pub async fn prompt(
+ &self,
+ run_id: String,
+ attempt: u32,
+ prompt: String,
+ cancellation: CancellationToken,
+ ) -> Result {
+ self.prompt_with_operator(run_id, attempt, prompt, cancellation, None)
+ .await
+ }
+
+ pub async fn prompt_with_operator(
+ &self,
+ run_id: String,
+ attempt: u32,
+ prompt: String,
+ cancellation: CancellationToken,
+ operator_requests: Option>,
+ ) -> Result {
+ let (reply, receive) = oneshot::channel();
+ self.commands
+ .try_send(Command::Prompt {
+ generation: self.generation,
+ run_id,
+ attempt,
+ prompt,
+ cancellation,
+ operator_requests,
+ reply,
+ })
+ .map_err(channel_error)?;
+ receive.await.map_err(|_| HostError::Unavailable)?
+ }
+
+ /// Invoke only a registered operation on this owner and generation. The
+ /// session ID and wire method are resolved inside the host actor.
+ pub async fn operation(
+ &self,
+ run_id: String,
+ operation_id: String,
+ arguments: serde_json::Value,
+ ) -> Result {
+ let (reply, receive) = oneshot::channel();
+ self.commands
+ .try_send(Command::Operation {
+ generation: self.generation,
+ run_id,
+ operation_id,
+ arguments,
+ reply,
+ })
+ .map_err(channel_error)?;
+ receive.await.map_err(|_| HostError::Unavailable)?
+ }
+}
+fn channel_error(error: mpsc::error::TrySendError) -> HostError {
+ match error {
+ mpsc::error::TrySendError::Closed(_) => HostError::Unavailable,
+ mpsc::error::TrySendError::Full(_) => HostError::ResourceLimit,
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "action", rename_all = "snake_case")]
+pub enum SessionControl {
+ Inspect,
+ Attach,
+ Renew { lease_id: String },
+ Release { lease_id: String },
+ Retire,
+}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(tag = "kind", rename_all = "snake_case")]
+pub enum ControlResult {
+ Snapshot(Box),
+ Lease { lease_id: String, ttl_ms: u64 },
+ Released,
+}
+
+enum Command {
+ Operation {
+ generation: u64,
+ run_id: String,
+ operation_id: String,
+ arguments: serde_json::Value,
+ reply: oneshot::Sender>,
+ },
+ Control {
+ generation: u64,
+ action: SessionControl,
+ reply: oneshot::Sender>,
+ },
+ Prompt {
+ generation: u64,
+ run_id: String,
+ attempt: u32,
+ prompt: String,
+ cancellation: CancellationToken,
+ operator_requests: Option>,
+ reply: oneshot::Sender>,
+ },
+}
+enum HostCommand {
+ Open {
+ launch: Box,
+ reply: oneshot::Sender>,
+ },
+ Lookup {
+ owner_id: String,
+ generation: u64,
+ reply: oneshot::Sender>,
+ },
+}
+type LaunchCompletion = (
+ String,
+ oneshot::Sender>,
+ Result<(AcpSessionIdentity, SessionHandle), HostError>,
+);
+
+#[derive(Clone, Debug)]
+pub struct SessionHost {
+ commands: mpsc::Sender,
+}
+impl SessionHost {
+ pub fn new(policy: RetentionPolicy) -> Result {
+ if !(1..=128).contains(&policy.max_sessions)
+ || !(1..=128).contains(&policy.max_attachments)
+ || policy.idle_timeout.is_zero()
+ || policy.attachment_ttl.is_zero()
+ || policy.idle_timeout > Duration::from_secs(86400)
+ || policy.attachment_ttl > Duration::from_secs(3600)
+ {
+ return Err(HostError::ResourceLimit);
+ }
+ let (commands, mut receive) = mpsc::channel(32);
+ tokio::spawn(async move {
+ let mut sessions: BTreeMap =
+ BTreeMap::new();
+ let mut starting = FuturesUnordered::new();
+ let mut pending = std::collections::BTreeSet::new();
+ loop {
+ let command = tokio::select! {
+ completed = starting.next(), if !starting.is_empty() => {
+ let (key, reply, result): LaunchCompletion = completed.expect("pending launch");
+ pending.remove(&key);
+ if let Ok((identity, handle)) = &result { sessions.insert(key, (identity.clone(), handle.clone())); }
+ let _ = reply.send(result.map(|(_, handle)| handle));
+ continue;
+ }
+ command = receive.recv() => match command { Some(command) => command, None => break },
+ };
+ sessions.retain(|_, (_, handle)| !handle.commands.is_closed());
+ match command {
+ HostCommand::Lookup {
+ owner_id,
+ generation,
+ reply,
+ } => {
+ let result = sessions
+ .values()
+ .find(|(_, h)| h.owner_id == owner_id)
+ .ok_or(HostError::Unavailable)
+ .and_then(|(_, h)| {
+ if h.generation == generation {
+ Ok(h.clone())
+ } else {
+ Err(HostError::IdentityMismatch)
+ }
+ });
+ let _ = reply.send(result);
+ }
+ HostCommand::Open { mut launch, reply } => {
+ if launch.expected_session_id.is_some() && !launch.require_persistence {
+ let _ = reply.send(Err(HostError::PersistenceUnsupported));
+ continue;
+ }
+ let key = launch.workspace.issue_id().to_owned();
+ let fingerprint = profile_fingerprint(
+ &launch.profile,
+ &launch.context.services,
+ &launch.limits,
+ );
+ let Ok(fingerprint) = fingerprint else {
+ let _ = reply.send(Err(HostError::Persistence));
+ continue;
+ };
+ launch.identity.profile_fingerprint = fingerprint;
+ if let Err(error) =
+ super::validate_launch(&launch.profile, &launch.context, &launch.limits)
+ {
+ let _ = reply.send(Err(HostError::Client(error.to_string())));
+ continue;
+ }
+ if launch.context.issue_workspace != launch.identity.workspace_path
+ || Some(launch.context.workspace_key.as_str())
+ != launch
+ .workspace
+ .workspace_path()
+ .file_name()
+ .and_then(|name| name.to_str())
+ {
+ let _ = reply.send(Err(HostError::IdentityMismatch));
+ continue;
+ }
+ if pending.contains(&key) {
+ let _ = reply.send(Err(HostError::Busy));
+ continue;
+ }
+ if let Some((identity, handle)) = sessions.get(&key) {
+ if launch.require_persistence && !handle.persistence_supported {
+ let _ = reply.send(Err(HostError::PersistenceUnsupported));
+ continue;
+ }
+ let mut expected = launch.identity.clone();
+ expected.generation = identity.generation;
+ expected.run_id.clone_from(&identity.run_id);
+ expected.attempt = identity.attempt;
+ let result = if expected == *identity
+ && (launch.identity.generation == 0
+ || launch.identity.generation == handle.generation)
+ {
+ Ok(handle.clone())
+ } else {
+ Err(HostError::IdentityMismatch)
+ };
+ let _ = reply.send(result);
+ continue;
+ }
+ if sessions.len() + pending.len() >= policy.max_sessions {
+ let _ = reply.send(Err(HostError::ResourceLimit));
+ continue;
+ }
+ pending.insert(key.clone());
+ let policy = policy.clone();
+ starting.push(async move { (key, reply, start(*launch, policy).await) });
+ }
+ }
+ }
+ });
+ Ok(Self { commands })
+ }
+ pub async fn open(&self, launch: SessionLaunch) -> Result {
+ let (reply, receive) = oneshot::channel();
+ self.commands
+ .try_send(HostCommand::Open {
+ launch: Box::new(launch),
+ reply,
+ })
+ .map_err(channel_error)?;
+ receive.await.map_err(|_| HostError::Unavailable)?
+ }
+ pub async fn lookup(
+ &self,
+ owner_id: String,
+ generation: u64,
+ ) -> Result {
+ let (reply, receive) = oneshot::channel();
+ self.commands
+ .try_send(HostCommand::Lookup {
+ owner_id,
+ generation,
+ reply,
+ })
+ .map_err(channel_error)?;
+ receive.await.map_err(|_| HostError::Unavailable)?
+ }
+}
+
+async fn start(
+ launch: SessionLaunch,
+ policy: RetentionPolicy,
+) -> Result<(AcpSessionIdentity, SessionHandle), HostError> {
+ super::validate_launch(&launch.profile, &launch.context, &launch.limits)
+ .map_err(|e| HostError::Client(e.to_string()))?;
+ if launch.context.issue_workspace != launch.workspace.workspace_path()
+ || Some(launch.context.workspace_key.as_str())
+ != launch
+ .workspace
+ .workspace_path()
+ .file_name()
+ .and_then(|name| name.to_str())
+ || launch.identity.workspace_path != launch.workspace.workspace_path()
+ {
+ return Err(HostError::IdentityMismatch);
+ }
+ let durable = if let Some(expected) = launch.expected_session_id {
+ Durability::open_bound_parent(launch.manager, launch.workspace, launch.identity, expected)
+ .await
+ } else {
+ Durability::open(launch.manager, launch.workspace, launch.identity).await
+ }
+ .map_err(HostError::from)?;
+ let identity = durable.state().identity.clone();
+ let (commands, receive) = mpsc::channel(16);
+ let (events, _) = broadcast::channel(
+ launch
+ .limits
+ .queued_frames
+ .min((64 * 1024 * 1024 / launch.limits.queued_bytes).max(1)),
+ );
+ let history = Arc::new(Mutex::new(EventHistory {
+ events: VecDeque::new(),
+ bytes: 0,
+ max_bytes: launch.limits.queued_bytes,
+ max_frames: launch.limits.queued_frames,
+ truncated: false,
+ latest_cursor: None,
+ }));
+ let mut handle = SessionHandle {
+ owner_id: durable.state().owner_id.clone(),
+ generation: identity.generation,
+ commands,
+ events: events.clone(),
+ history: history.clone(),
+ persistence_supported: false,
+ };
+ let (ready, ready_receive) = oneshot::channel();
+ let publisher = EventPublisher {
+ tx: events,
+ binding: Arc::new(Mutex::new((
+ identity.generation,
+ identity.run_id.clone(),
+ false,
+ ))),
+ replay_request: Arc::new(Mutex::new(None)),
+ history,
+ };
+ let mut driver = SessionDriver {
+ durable,
+ receive,
+ publisher,
+ policy,
+ require_persistence: launch.require_persistence,
+ ready: Some(ready),
+ attachments: BTreeMap::new(),
+ idle_since: tokio::time::Instant::now(),
+ retire_reply: None,
+ reset_reason: None,
+ operator_router: Arc::new(Mutex::new(None)),
+ };
+ tokio::spawn(async move {
+ let result = super::run_connection(
+ &launch.profile,
+ launch.context,
+ String::new(),
+ CancellationToken::new(),
+ None,
+ launch.limits,
+ Some(&mut driver),
+ )
+ .await;
+ let error = match &result {
+ Ok(result) => result.outcome.as_ref().err().map(ToString::to_string),
+ Err(error) => Some(error.to_string()),
+ };
+ // A failed connection cannot establish remote quiescence. A submitted marker
+ // survives even if the outcome checkpoint fails, and prevents another prompt.
+ let checkpointed = if result.as_ref().is_ok_and(|r| {
+ r.process_reaped
+ && (r.process_tree_signal_error.is_none() || driver.durable.process_is_absent())
+ }) || matches!(
+ result,
+ Err(ClientError::InvalidConfiguration(_)
+ | ClientError::InvalidWorkspace
+ | ClientError::Launch(_)
+ | ClientError::CancelledBeforePrompt)
+ ) {
+ driver.durable.stopped().await.is_ok()
+ } else {
+ false
+ };
+ if driver.durable.state().status == AcpSessionStatus::Submitted {
+ let _ = driver.durable.uncertain().await;
+ }
+ if let Some(ready) = driver.ready.take() {
+ let _ = ready.send(Err(HostError::Client(
+ error
+ .clone()
+ .unwrap_or_else(|| "owner exited during setup".into()),
+ )));
+ }
+ let cleanup_ready = error.is_none()
+ && checkpointed
+ && matches!(
+ driver.durable.state().status,
+ AcpSessionStatus::Ready | AcpSessionStatus::Finished
+ );
+ let mut ended_snapshot = driver.snapshot(false);
+ ended_snapshot.live = false;
+ ended_snapshot.retirement_eligible = false;
+ ended_snapshot.state.recovery = AcpRecovery::TranscriptOnly;
+ let _ = driver.publisher.tx.send(SessionEvent::State {
+ snapshot: Box::new(ended_snapshot),
+ });
+ let reply = driver.retire_reply.take();
+ let publisher = driver.publisher.clone();
+ let generation = driver.durable.state().identity.generation;
+ drop(driver); // Release the owner lock and close commands before acknowledging retirement.
+ let _ = publisher.tx.send(SessionEvent::Ended {
+ generation,
+ cleanup_ready,
+ detail: error,
+ });
+ if let Some(reply) = reply {
+ let _ = reply.send(if cleanup_ready {
+ Ok(ControlResult::Released)
+ } else {
+ Err(HostError::Unavailable)
+ });
+ }
+ });
+ handle.persistence_supported = ready_receive.await.map_err(|_| HostError::Unavailable)??;
+ Ok((identity, handle))
+}
+
+type PromptFuture = Pin> + Send>>;
+struct ActivePrompt {
+ result: PromptFuture,
+ reply: oneshot::Sender>,
+ cancellation: CancellationToken,
+}
+
+struct PreparingPrompt {
+ result: Pin> + Send>>,
+ run_id: String,
+ attempt: u32,
+ prompt: String,
+ cancellation: CancellationToken,
+ forced: CancellationToken,
+ callback_epoch: CancellationToken,
+ operator_requests: Option>,
+ reply: oneshot::Sender>,
+}
+
+pub(super) struct SessionDriver {
+ durable: Durability,
+ receive: mpsc::Receiver,
+ pub(super) publisher: EventPublisher,
+ policy: RetentionPolicy,
+ require_persistence: bool,
+ ready: Option>>,
+ attachments: BTreeMap,
+ idle_since: tokio::time::Instant,
+ retire_reply: Option>>,
+ reset_reason: Option,
+ pub(super) operator_router: super::operator::OperatorRouter,
+}
+impl SessionDriver {
+ pub(super) async fn launched(&mut self, pid: Option) -> Result<(), ClientError> {
+ self.durable
+ .launched(pid.ok_or(ClientError::Teardown)?)
+ .await
+ .map_err(|_| ClientError::Setup("process ownership checkpoint failed".into()))
+ }
+ pub(super) fn reset_missing_session(&mut self, error: &agent_client_protocol::Error) -> bool {
+ if self.require_persistence
+ || error.code != agent_client_protocol::schema::v1::ErrorCode::ResourceNotFound
+ {
+ return false;
+ }
+ self.durable.state_mut().recovery = AcpRecovery::Fresh;
+ self.reset_reason = Some(
+ "agent no longer has persisted context; fresh full-context prompt required".into(),
+ );
+ self.publisher.end_replay();
+ true
+ }
+ pub(super) fn restoration(
+ &mut self,
+ initialization: &InitializeResponse,
+ ) -> Result